这是我参与更文挑战的第11天,活动详情查看: 更文挑战。
前言
前面我们学习了模板编译中的解析器,这次我们将学习优化器。
优化器
解析器的作用是将HTML模板解析成AST,而优化器的作用是在AST中找出静态子树并打上标记。
什么是静态子树?
静态子树
指的是那些在 AST
中永远都不会发生变化的节点。例如,一个纯文本节点就是静态子树,而带变量的文本节点就不是静态子树,因为它会随着变量的变化而变化。
好处
标记静态子树有两点好处:
- 每次重新渲染时,不需要为静态子树创建新节点
- 在
虚拟DOM
中打补丁(patching)的过程可以跳过
为什么重新渲染时,不需要为静态子树创建新节点?
前面我们有讲过克隆节点
。在生成VNode的过程中,如果发现一个节点被标记为静态子树,那么除了首次渲染会生成节点之外,在重新渲染时并不会生成新的子节点树,而是克隆已存在的静态子树。
为什么在 patch 阶段可以跳过?
如果两个节点都是静态子树,就不需要进行对比与更新 DOM 的操作,直接跳过。因为静态子树是不可变的,不需要对比就知道它不可能发生变化。此外,直接跳过后续的各种对比可以节省 JavaScript 的运算成本。
内部实现
优化器的内部实现主要分为两个步骤:
- 在AST中找出所有
静态节点
并打上标记 - 在AST中找出所有
静态根节点
并打上标记
类似下面的节点就是静态节点:
<p>我是静态节点<p>
复制代码
对应到 AST 中,就是节点的static
为 true
,如果是静态根节点,staticRoot
也为true
。
找出所有静态节点并标记
递归 AST,使用 isStatic
函数来判断节点是否是静态节点,然后如果节点的类型等于1
,说明节点是元素节点
,那么循环该节点的子节点,调用 markStatic
函数用同样的处理逻辑来处理子节点:
function markStatic(node){
node.static = isStatic(node);
if (node.type === 1) {
for (let i = 0, l = node.children.length; i<l;i++){
const child = node.children[i];
markStatic(child);
}
}
}
复制代码
什么样的节点是静态节点?
当模板被解析器解析成AST时,会根据不同元素类型设置不同的type值
type | 说明 |
---|---|
1 | 元素节点 |
2 | 带变量的动态文本节点 |
3 | 不带变量的纯文本节点 |
显而易见,没有变量的文本节点肯定是静态节点,然后没有使用v-if
、v-else
、v-for
,或没有使用v-bind
也是静态节点。
自定义组件或者内置组件也不会是静态节点。
由于递归是从上向下依次标记的,如果父节点被标记为静态节点之后,子节点却被标记为动态节点,这时就会发生矛盾。因为静态子树中不应该只有它自己是静态节点,静态子树的所有子节点应该都是静态节点。因此,我们需要在子节点被打上标记之后重新校对当前节点的标记是否准确。
function markStatic(node){
node.static = isStatic(node);
if (node.type === 1) {
for (let i = 0, l = node.children.length; i<l;i++){
const child = node.children[i];
markStatic(child);
// 新增
if(!child.static){
node.static = false
}
}
}
}
复制代码
我们需要判断它是否是静态节点,如果不是,那么它的父节点也不可能是静态节点。
找出所有静态根节点并标记
找出静态根节点的过程与找出静态节点的过程类似,都是使用递归的方式。如果一个节点被判定为静态根节点,那么将不会继续向它的子级继续寻找。