通过解析器篇章的学习,我们知道,解析器的作用是将HTML模板解析成AST,而本章要介绍的优化器,则主要用来在AST中找出静态节点并打上标记。下面,我们通过提问回答的方式,详细了解一下优化器相关概念及工作原理。
一、模板编译中为什么需要优化器
理论上讲,有了AST就可直接生成render函数了。为什么还需要优化器呢?这当然是为了性能考虑。
我们通过优化器,找出静态节点,可以实现以下两点好处:
-
每次重新渲染时,不需要为静态节点创建新节点。
在前面虚拟DOM的学习中,我们知道,每次重新渲染,都会使用最新的状态生成一份全新的VNode与旧的VNode进行对比。而在生成vode的过程中,如果发现一个节点被标记为静态节点,那么除了首次渲染会生成节点外,在重新渲染时并不会生成新的子节点,而是克隆已经存在的静态节点。
-
在虚拟DOM的patch过程中可以跳过。
patch过程中,如果两个节点都是静态节点,就不需要进行对比与更新DOM的操作,而是可以直接跳过,这样既节省了JS运算成本,又减少了DOM的操作。
二、优化器所寻找的静态节点到底是什么
所谓静态节点,是指那些在AST中永远都不会发生变化的节点,也就是说,它一旦首次渲染成了,之后不管状态如何改变,它都不会变化了。例如:
<ul>
<li>我是静态节点,我永远不会发生变化</li>
<li>我是静态节点,我永远不会发生变化</li>
</ul>
在上面代码中,每个li标签里的内容都是不含任何变量的文本,它不随着状态改变而改变,所以该li标签就是一个静态节点。
而li标签的父节点ul标签,因其所有子节点都是静态节点,所以它也是静态节点,我们称之为静态根节点。
三、优化器内部是如何运作的
优化器的内部实现主要分为两个步骤:
1.在AST中找出所有静态节点并打上标记
2.在AST中找出所有静态根节点并打上标记;
落实到源码中,代码是这样实现的。
export function optimize (root: ?ASTElement, options: CompilerOptions) {
if (!root) return
// 标记静态节点
markStatic(root)
// 标记静态根节点
markStaticRoots(root, false)
}
四、优化器如何标记静态节点
要想从AST中找出所有静态节点,我们只需从根节点开始,先标记根节点是否为静态节点,然后看根节点如果是元素节点,那么就去向下递归它的子节点,子节点如果还有子节点那就继续向下递归,直到标记完所有节点。
所谓标记静态节点,就是为该节点加上static属性,如果该节点是静态节点,则属性值为true,如果不是静态节点,则属性值为fasle。源码如下:
function markStatic (node: ASTNode) {
node.static = isStatic(node)
if (node.type === 1) { // node.type为1时,代表该节点时元素节点
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i]
markStatic(child)
}
}
}
那么,如何找出一个静态节点呢?isStatic函数是关键,具体如下:
function isStatic (node: ASTNode): boolean {
// 当前节点为包含变量的动态文本节点
if (node.type === 2) {
return false
}
// 当前节点为不包含变量的纯文本节点
if (node.type === 3) {
return true
}
//当前节点为元素节点时,需要进一步判断
return !!(node.pre || (
!node.hasBindings && // no dynamic bindings
!node.if && !node.for && // not v-if or v-for or v-else
!isBuiltInTag(node.tag) && // not a built-in
isPlatformReservedTag(node.tag) && // not a component
!isDirectChildOfTemplateFor(node) &&
Object.keys(node).every(isStaticKey)
))
}
HTML解析器在调用钩子函数创建AST节点时会根据节点类型的不同为节点加上不同的type属性,来标记AST节点的节点类型。所以在判断一个节点是否为静态节点时首先会根据type值判断节点类型,如果type值为2,那么该节点是包含变量的动态文本节点,它就肯定不是静态节点,返回false。
如果type值为2,那么该节点是不包含变量的纯文本节点,它就肯定是静态节点,返回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之一。
- 不能使用动态绑定语法,即标签上不能有
五、优化器如何标记静态根节点
所谓标记静态根节点,就是为该节点加上staticRoot属性,如果该节点是静态根节点,则属性值为true,如果不是静态根节点,则属性值为fasle。
寻找静态根节点与寻找静态节点的逻辑类似,都是从AST根节点递归向下遍历寻找。其代码如下:
function markStaticRoots (node: ASTNode, isInFor: boolean) {
if (node.type === 1) {
if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) {
node.staticRoot = true
return
} else {
node.staticRoot = false
}
if (node.children) {
for (let i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for)
}
}
}
}
从代码我们可以看到,一个节点要想成为静态根节点,它必须满足以下要求:
- 节点本身必须是静态节点;
- 必须拥有子节点
children; - 子节点不能只是只有一个文本节点;
如果当前节点不是静态根节点,那就继续递归遍历它的子节点。