Vue模板编译器原理

模板渲染流程

  1. 通过正则把模板转换成 AST 抽象语法树
  2. 优化器优化,标记所有的静态节点后,交由代码生成器生成渲染代码,
  3. 通过渲染函数构建器将渲染代码构建成一个渲染函数
  4. 调用渲染函数,我们就可以得到目标模板的虚拟dom

AST抽象树的结构

AST的每个节点的结构如下:

1
2
3
4
5
6
7
8
9
{
tag: tagName, // 标签名
type: 1, // 元素类型
children: [], // 孩子列表
attrs, // 属性集合
parent: null, // 父元素
text: null // 文本节点内容
...
}

节点类型有:元素类型、文本类型、注释类型…

生成AST抽象语法树

模板引擎解析html字符串,每匹配到下面一种类型,就触发对应的钩子函数

  • 是否是开始标签,如:<div …> ;
  • 是否是结束标签,如:
;
  • 是否是一段文本,如:
    这是一段代码
    中的文字;
  • 是否是一个注释,如:
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    parseHTML(html, {
    start(tag, attrs, isUnary, startIndex, endIndex) {
    // 收集整理信息
    },
    end(tag, startIndex, endIndex) {
    // 收集整理信息
    },
    chars(text, startIndex, endIndex) {
    // 收集整理信息
    },
    comment(text, startIndex, endIndex) {
    // 收集整理信息
    },
    }

    从上面的代码可以看到,我们将模板字符串是否存在作为while的终止条件,我们每匹配到 一种情况,就会将相应的文本从html中删除掉,这样不断循环下去,就会不断的触发响应的钩子函数收集数据,直至html变成空字符串退出循环,此时,我们已经将整个文本都解析完了。

    钩子函数start

    • 根据传过来的标签名创建抽象语法树元素节点
    • 检查一些非法属性或标签,并对一些特殊情况做预处理
    • 解析attrs
    • 解析指令v-if,v-for,v-once等
    • 如果不是自闭标签的话,将当前元素加入到栈中,用于维护元素间的父子关系

    钩子函数end

    • 将栈顶元素弹出(因为当前标签已经结束后了,栈顶存的就是当前标签)
    • 重新更正父级标签(因为当前标签已经结束,说明他的子节点也都解析完了,父标签不在是当前标签了,父级标签有重新变回当前标签的父级标签)
    • 关闭标签,此时对if条件分支进行一些补充以及进行一些收尾工作等

    钩子函数chars

    • 创建抽象语法树文本节点
    • 将这个文本节点加入到父节点的children中

    钩子函数comment

    • 创建抽象语法树注释节点
    • 只要注释节点存在父级,就把注释节点加入到父级节点的children中

    举个例子:

    1
    2
    3
    4
    5
    6
    7
    <div>
    <p>这是一段文字</p>
    <ul>
    <li>选项1</li>
    <li>选项2</li>
    </ul>
    </div>

    当我们解析到<div>时,将div加入到栈中,此时栈中只有div一个元素,然后继续解析,解析到<p>s的时候,我们再把p也加入到栈中,此时栈中有div和p两个元素,栈顶的元素是p。再往下解析,解析到一段文字,触发chars收集并生成抽象语法树文本节点,那么,此时,这个文本节点的父级是什么呢?显而易见,就是我们栈顶的元素p。所以我们将这个文本节点加入到p的children中即可。继续解析,发现</p>结束标签,我们将栈顶元素p弹出来,也就是说,现在我们的栈里又只有一个div了,同理,下面的ul和li也是这样操作。当最后解析到</div>时,栈里也就只有一个div元素了,将栈顶的div弹出,栈空了,已经解析完了。流程如下:

    参考:

    1. Vue模板编译原理

    2. Vue源码学习之模板编译器原理

    3. HTMLParser 的实现和使用

    4. 模板引擎实现原理(Vue篇)