模版解析
Vue 模版解析就是将开发者输入的模版字符串转换成 render 函数。此过程分为三个阶段
- 解析字符串生成虚拟 DOM
- 执行编译优化,递归遍历 AST,标记静态节点和静态根节点
- 生成 render 函数
总流程
解析器
HTML 解析器
// 伪代码
parseHTML(template, {
start(tag, attrs, unary) {
// 每当解析到标签的开始位置时,触发该函数
},
end() {
// 每当解析到标签的结束位置时,触发该函数
},
chars(text) {
// 每当解析到文本时,触发该函数
},
comment(text) {
// 每当解析到注释时,触发该函数
}
});
- 接收到模版字符串
tpl = `<div>
<h1>我是Berwin</h1>
<p>我今年23岁</p>
</div>`;
通过正则表达式匹配标签(开始标签、结束标签、文本标签等),调用对应的钩子函数(start、end、chars)
- 开始标签中匹配出对应的属性值,拿到这些属性构建虚拟节点
- 匹配到开始标签,将节点入栈(当前栈顶的节点就是该节点的父亲)
- 匹配到结束标签,将栈顶的节点出栈
删除匹配到的标签
优化器
对一些静态的节点(更新时 DOM 不会变化的节点)打标记。
好处:
- 使 DIFF 的过程中可以跳过,既节省了 JS 运算成本,又减少了 DOM 的操作。
- 每次重新渲染的时候,静态节点可以直接复用,不用重新生成
TIP
每次重新渲染,都会使用最新的状态生成一份全新的 VNode 与旧的 VNode 进行对比。而在生成 vode 的过程中,如果发现一个节点被标记为静态节点,那么除了首次渲染会生成节点外,在重新渲染时并不会生成新的子节点,而是克隆已经存在的静态节点。
实现
1. 标记静态节点
递归遍历 AST,根据 isStatic()
, 在当前节点上增加 static
属性
HTML 解析器在调用钩子函数创建 AST 节点时会根据节点类型的不同为节点加上不同的 type 属性,来标记 AST 节点的节点类型。所以在判断一个节点是否为静态节点时首先会根据 type 值判断节点类型。
type: 2
- 包含变量的动态文本节点,=>
false
- 仅纯文本节点 =>
true
- 包含变量的动态文本节点,=>
type: 1
说明该节点是元素节点- 使用了 v-pre 指令,那就断定它是静态节点;
- 没有使用 v-pre 指令,那它要成为静态节点必须满足:
- 不能使用动态绑定语法,即标签上不能有 v-、@、:开头的属性;
- 不能使用 v-if、v-else、v-for 指令;
- 不能是内置组件,即标签名不能是 slot 和 component;
- 标签名必须是平台保留标签,即不能是组件;
- 当前节点的父节点不能是带有 v-for 的 template 标签;
- 节点的所有属性的 key 都必须是静态节点才有的 key,注:静态节点的 key 是有限的,它只能是 type,tag,attrsList,attrsMap,plain,parent,children,attrs 之一。
2. 标记静态根节点 递归遍历 AST,根据子节点类型判断,在当前节点标记 staticRoot
- 节点本身必须是静态节点
- 必须拥有子节点 children
- 子节点不能只是只有一个文本节点
生成器
根据生成的 AST 拼接成对应的 render 函数。
with (this) {
return _c(
'div',
{
attrs: { id: 'el' }
}[(_c('p'), [_v('Hello ' + _s(name))])]
);
}
为什么使用 with 语句?
vue 并没有对模板中的 javascript 表达式进行 ast 语法分析,如果要移除 with
,就需要对 javascript 表达式进行 ast 语法分析,并且还需要一个专门的解释器对 ast 语法树进行解释,这样就会导致存在两个并行的解析器,这样维护成本高,还可能会有潜在的 bug 风险。
template 和 render 函数的区别
Vue 支持 template 模版和 render 函数两种渲染方式, 如果同时使用两者,会优先采用 render 的方式。 本质上,模版最终也会编译成 render 函数。 模版的写法更贴近于 html,因此其看起来很直观,render 函数本身是 js,因此更灵活。
TIP
Vue 分为完整版(vue.min.js)和运行时(vue.runtime.min.js)两个版本
- 完整版内置了 complier,因此可以在运行时处理 template 语法,但是其包体积较大。
- 运行时仅支持 render 函数,是官方默认的版本。(一般情况下模版编译是在项目本地打包的时候处理)。