Vue 是如何进行模板编译的?(三)

前言

上一节先把比较难以理解的一些正则先给简单介绍了,这一节原本是想来把可个Htmlparser的源码刨一下,但因为作者尤大大也是参考了 http://erik.eae.net/simplehtmlparser/simplehtmlparser.js 这个 simplehtmlparser 来改出来的,我们何不以 simplehtmlparser 来入手,自己 DIY,看能玩出什么花样,其实在写此文章前,我已经按自己的思路把 simplehtmlparser 简单改了一下,然后按自己的思路用其生成出一个自己想要的简单 AST 树。当然,尤大大的 vue 用到的 AST 非常强大,但如果用于学习的话,没必要全部按 Vue 源码那个来,因为那代码已经被社区 N 个人反复修改、修正过的,读起来不好理解,要通过最简单的东西一步步展开才好理解作者最初的思路

自己 DIY 要生成的目标 – 简单AST树

如下,想查看全部源码点这里

var myAST = {
  type: 1, // 1是标签, 2是文本
  attrs: [], // 属性列表,如{name: "class", value: "head__title"}等
  tagName: 'div', // 标签名,如div, a, 等等
  children: [], // myAST子列表
  nodeId: 1 // 每个myAST有一个唯一的id
}

输入的待处理的 html 串如下

<div id="vue-template-content">
    <input type="number" name="phone" class="text" id="loginname" placeholder="请输入手机号码" maxlength="11">
    <!--- <div>测试comment</div> --->
    <h1>自己动手做一下Html Parser => AST,注意:使用的是只有一个根节点的模式</h1>
    <div class="container">
        <div class="container">
            <div class="redbox infinite rotate"></div>
            <div class="bluebox fadeInPendingFadeOutUp"></div>
            <div class="orangebox fadeInUp2D"></div>
        </div>
    </div>
    <footer id="footer" style="text-align:center;font-size:12px;padding:50px 0;">
        <div class="outer">
            <div id="footer-info">
                <div class="footer-left"><a target="_blank" href="http://www.a4z.cn/pui">返回 PUI 首页</a> |<a
                        target="_blank" href="mailto:kbl_1794@qq.com">联系我们</a><br><br> Copyright © 2015-<span
                        id="year_today">201<9</span> a4z>.cn |<a href="http://www.miibeian.gov.cn/"
                                target="_blank">粤ICP备18002963号</a></div>
            </div>
        </div>
    </footer>
</div>

SimpleHtmlParser 源码

// 引用自 https://github.com/nelsonkuang/markdown/blob/master/temp/html-parser.html
/*
var handler ={
  startElement:   function (sTagName, oAttrs) {},
  endElement:     function (sTagName) {},
    characters:		function (s) {},
    comment:		function (s) {}
};
*/

function SimpleHtmlParser() {}

SimpleHtmlParser.prototype = {

  handler: null,

  // regexps

  startTagRe: /^<([^>\s\/]+)((\s+[^=>\s]+(\s*=\s*((\"[^"]*\")|(\'[^']*\')|[^>\s]+))?)*)\s*\/?\s*>/m,
  endTagRe: /^<\/([^>\s]+)[^>]*>/m,
  attrRe: /([^=\s]+)(\s*=\s*((\"([^"]*)\")|(\'([^']*)\')|[^>\s]+))?/gm,

  parse: function (s, oHandler) {
    if (oHandler)
      this.contentHandler = oHandler;

    var i = 0;
    var res, lc, lm, rc, index, lastMatch, lastIndex;
    var treatAsChars = false;
    var oThis = this;
    while (s.length > 0) {
      // Comment
      if (s.substring(0, 4) == "<!--") {
        index = s.indexOf("-->");
        if (index != -1) {
          this.contentHandler.comment(s.substring(4, index));
          s = s.substring(index + 3);
          treatAsChars = false;
        } else {
          treatAsChars = true;
        }
      }

      // end tag
      else if (s.substring(0, 2) == "</") {
        // 改造了这里,因为RegExp.leftContext,RegExp.rightContext等MDN上不建议应用于生产环境
        // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/RegExp/rightContext
        // if (this.endTagRe.test(s)) {
        lastMatch = s.match(this.endTagRe)
        if (lastMatch) {
          lastIndex = s.search(this.endTagRe)
          // lc = RegExp.leftContext;
          lm = lastMatch[0]
          // rc = RegExp.rightContext;
          rc = s.substring(lastIndex + lastMatch[0].length)

          lm.replace(this.endTagRe, function () {
            return oThis.parseEndTag.apply(oThis, arguments);
          });

          s = rc;
          treatAsChars = false;
        } else {
          treatAsChars = true;
        }
      }
      // start tag
      else if (s.charAt(0) == "<") {
        // 改造了这里,因为RegExp.leftContext,RegExp.rightContext等MDN上不建议应用于生产环境
        // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/RegExp/rightContext
        lastMatch = s.match(this.startTagRe)
        if (lastMatch) {
          lastIndex = s.search(this.startTagRe)
          // lc = RegExp.leftContext;
          lm = lastMatch[0]
          rc = s.substring(lastIndex + lastMatch[0].length)
          // console.log(rc)

          lm.replace(this.startTagRe, function () {
            return oThis.parseStartTag.apply(oThis, arguments);
          });

          s = rc;
          treatAsChars = false;
        } else {
          treatAsChars = true;
        }
      }

      if (treatAsChars) {
        index = s.indexOf("<");
        if (index == -1) {
          this.contentHandler.characters(s);
          s = "";
        } else {
          this.contentHandler.characters(s.substring(0, index));
          s = s.substring(index);
        }
      }

      treatAsChars = true;
    }
  },

  parseStartTag: function (sTag, sTagName, sRest) {
    var attrs = this.parseAttributes(sTagName, sRest);
    this.contentHandler.startElement(sTagName, attrs);
  },

  parseEndTag: function (sTag, sTagName) {
    // console.log(sTag)
    this.contentHandler.endElement(sTagName);
  },

  parseAttributes: function (sTagName, s) {
    var oThis = this;
    var attrs = [];
    // 下面这种用法太NB,直接把replace的正则替换回调的所有参数用上
    s.replace(this.attrRe, function (a0, a1, a2, a3, a4, a5, a6) {
      attrs.push(oThis.parseAttribute(sTagName, a0, a1, a2, a3, a4, a5, a6));
    });
    return attrs;
  },

  parseAttribute: function (sTagName, sAttribute, sName) {
    var value = "";
    if (arguments[7])
      value = arguments[8];
    else if (arguments[5])
      value = arguments[6];
    else if (arguments[3])
      value = arguments[4];

    var empty = !value && !arguments[3];
    return {
      name: sName,
      value: empty ? null : value
    };
  }
};

调用上面的 Simple Html Parser 生成一棵 AST 树

这里为了方便调试,不放过每个标签节点或者文本,全部使用 console.log 打印出来,最后结果也可以通过控制台直接看到,我们不必太在意什么 flow 或者 typescript 哈,因为这些都是 js 的语法糖,要实实在在的使用最方便自己调试的方式来写出代码才是硬道理。当然,后续如果团队合作时,再把自己的代码加上语法糖再封装出来即可,不要为了使用 typescript 而使用 typescript 哈。一开始就把代码写复杂了也是一个误区。

调用上面的 SimpleHtmlParser 方法生成 AST 树

$(function () {
  var ps = new SimpleHtmlParser()
  var html = $('article').html()
  var startElementCount = 0,
      endElementCount = 0,
      charactersCount = 0,
      elementStack = [],
      // endElementStack = [],
      // charactersStack = [],
      commentCount = 0,
      currentNode = null,
      parentNode = null,
      nodeId = 0,
      // specialTags = ['br', 'input']
      root = Object.create(
          null
          ) // node : {type = 1'tag' | 2'text', tagName?=String, children = [], attrs = [], text?= String, nodeId=String}
  ps.parse(html, {
      startElement: function (sTagName, oAttrs) {
          console.log('startElement', ++startElementCount)
          console.log(sTagName)
          console.log(oAttrs)
          var tagNode = Object.create(null) // 用干净的 object 优化性能
          tagNode.type = 1
          tagNode.attrs = oAttrs
          tagNode.tagName = sTagName
          tagNode.children = []
          tagNode.nodeId = ++nodeId
          /*                     var tagNode = {
                                  type: 1,
                                  attrs: oAttrs,
                                  tagName: sTagName,
                                  children: [],
                                  nodeId: ++nodeId
                              } */
          if (sTagName != 'br' && sTagName != 'input') { // br等等特殊标签处理
              elementStack.push(tagNode)
          }
          if (startElementCount == 1) {
              root = tagNode
              parentNode = tagNode
              // console.log(root, parentNode)
          } else {
              // 处理插入树结构
              insertNodeIntoTree(root, parentNode, tagNode)
              if (sTagName != 'br' && sTagName != 'input') { // br 等等特殊标签处理,不能作为父节点
                  parentNode = tagNode
              }
          }
      },
      endElement: function (sTagName) {
          console.log('endElement', ++endElementCount)
          console.log(sTagName)
          var len = elementStack.length
          if (len > 0) {
              if (elementStack[elementStack.length - 1].tagName == sTagName) {
                  // 处理一个标签闭合完成
                  elementStack.pop()
                  parentNode = elementStack[elementStack.length - 1]
              } else {
                  // 出现未闭合标签,报错
              }
          }
      },
      characters: function (s) {
          console.log('characters', ++charactersCount)
          console.log(s)
          var textNode = Object.create(null) // 用干净的 object 优化性能
          textNode.type = 2
          textNode.text = s
          textNode.nodeId = ++nodeId
          /*                     var textNode = {
                                  type: 2,
                                  text: s,
                                  nodeId: ++nodeId
                              } */
          // 处理插入树结构
          if (parentNode && parentNode.nodeId) { // 不处理空格
              insertNodeIntoTree(root, parentNode, textNode)
          }
      },
      comment: function (s) {
          console.log('comment', ++commentCount)
          console.log(s)
      }
  })

  function insertNodeIntoTree(tRoot, parentTNode, tNode) { // 递归为每个儿子节点找爸爸,找到到添加到 children 数组里
      var foundOne = false // 添加标记,优化了性能,不然就算找到了位置,其他的树叉继续去递归遍历就浪费内存
      var dive = function (rNode, pNode, node) { // 递归查找插入的位置
          if (!foundOne && rNode.nodeId == pNode.nodeId) {
              rNode.children.push(node)
              foundOne = true
          } else if (!foundOne && rNode.children && rNode.children.length > 0) {
              rNode.children.forEach((subRNode) => {
                  dive(subRNode, pNode, node)
              })
          }
      }
      dive(tRoot, parentTNode, tNode) // 开始
  }
  console.log(root) // => 生成一棵完整和 AST 树
});

生成结果如下:

生成一个 AST 树结果
生成一个 AST 树结果

总结

到这里整个 demo 就算完成了。这里面有个关键的思路,就是给每个节点添加一下 nodeId,然后通过这个 nodeId,递归为每个儿子节点找爸爸,找到到添加到 children 数组里,这种思路方式是本人原创的,性能也初步优化过一下,可能与 vue 源码上看到的处理方式有点不一样。但条条大路通罗马嘛,只有最适合自己的,才是最好的。下一篇将构思如何把上面生成的《AST 转化与 renderString 或者转化成 Virtual Dom》吧。

作者: 博主

Talk is cheap, show me the code!

发表评论

电子邮件地址不会被公开。

Captcha Code