携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第18天,点击查看活动详情
在虚拟DOM这篇文章中,我们了解到当响应性起作用,通知到变化后,虚拟DOM会调用patch方法对虚拟DOM进行比对,找出有变化的节点进行渲染,而渲染的时候为了提高性能,减少不必要的比对开销,在上一步生成的AST抽象树之后,我们还要对这个树进行优化,在优化之后,即可生成render函数最终将模板转化为虚拟DOM。
抽象树的优化
静态节点和静态根节点
优化抽象树来提高性能的主要操作就是标记静态节点和静态根节点,什么是静态节点和静态根节点呢,我们来一个例子:
<div>
文字
<span>文字</span>
<span>文字</span>
<span>文字</span>
<span>文字</span>
</div>
如果说上面这段代码是vue的模板,那么文字节点文字,以及元素节点<span>文字</span>中不包含任何变量和vue的标记,也就是说,无论数据如何变化,它们都不会发生任何改变,因此它们就被称作静态节点。而顺着静态节点往上找,这个<div>中的所有子元素都是静态节点,因此称之为静态根节点。
所以说,抽象树的优化主要只做了两件事:
- 标记静态节点
- 标记静态根节点
方法入口
抽象树优化的代码在src\compiler\optimizer.ts文件中,主要方法入口是optimize方法:
// 源码文件src\compiler\optimizer.ts
export function optimize(
root: ASTElement | null | undefined,
options: CompilerOptions
) {
if (!root) return
// 处理传入的根节点判断方法或默认
isStaticKey = genStaticKeysCached(options.staticKeys || '')
// 处理传入的保留标签或留空
isPlatformReservedTag = options.isReservedTag || no
// 先标记静态节点
markStatic(root)
// 再标记静态根节点
markStaticRoots(root, false)
}
标记静态节点
标记静态节点主要使用了markStatic和isStatic方法:
// 源码文件src\compiler\optimizer.ts
function markStatic(node: ASTNode) {
node.static = isStatic(node)
if (node.type === 1) {
// do not make component slot content static. this avoids
// 1. components not able to mutate slot nodes
// 2. static slot content fails for hot-reloading
if (
!isPlatformReservedTag(node.tag) &&
node.tag !== 'slot' &&
node.attrsMap['inline-template'] == null
) {
return
}
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i]
markStatic(child)
if (!child.static) {
node.static = false
}
}
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
const block = node.ifConditions[i].block
markStatic(block)
if (!block.static) {
node.static = false
}
}
}
}
}
function isStatic(node: ASTNode): boolean {
if (node.type === 2) {
// expression
return false
}
if (node.type === 3) {
// text
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))
)
}
首先直接调用isStatic方法对当前节点进行判断,判断逻辑如下:
- 动态文本节点标记为非静态节点
- 静态文本节点标记为静态节点
- 标记了
pre的元素节点标记为静态节点 - 其他情况必须以下条件全部符合才为静态节点,否则为非静态节点
- 没有动态属性绑定
- 没有
v-if,v-for,v-else命令存在 - 不是内部保留组件
- 不是组件
- 父节点不带有
v-for命令 - 所包含的属性必须在
isStatickey内
isStatickey定义了静态节点包含的有限个属性,分别是:type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap
之后再递归遍历节点的子节点,一旦发现了非静态节点,则整个父元素就要被标记为非静态节点,因为父元素中存在了“变化”,任何一个元素节点只要有一个部分能够发生变化,那就不能被当作静态节点来处理,比如:
<div>
123
<span>145</span>
<span>{{num}}</span>
</div>
根据一开始的判断,我们标记这个div为静态节点,然后开始遍历子节点,前两个子节点均为静态节点,而到第三个节点的时候,第三个节点为非静态节点,此时我们无法将div整体做为静态节点处理,因此将div重新标记为非静态节点。
之后,还要对v-if,v-else,v-else-if的组合内部进行处理,依然是“连坐”制度,只要有一个子节点为非静态节点,则整体都要被标记为非静态节点。
标记静态根节点
有人可能要问了,既然静态节点都标记好了,直接进行下一步生成render就好了呗,为什么还要进一步标记根节点呢,实际上这是进一步对性能的优化,我们在后面的生成方法中可以看到,优先判断静态节点时首先判断的是其是否为静态根节点的属性staticRoot,因为这样可以把整个静态根节点整体进行处理,而不用再次去挨个处理里面的每个静态节点。
标记根节点的方式跟标记静态节点大同小异:
// 源码文件src\compiler\optimizer.ts
function markStaticRoots(node: ASTNode, isInFor: boolean) {
if (node.type === 1) {
if (node.static || node.once) {
node.staticInFor = isInFor
}
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)
}
}
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
markStaticRoots(node.ifConditions[i].block, isInFor)
}
}
}
}
其中,判断静态根节点有三个先决条件:
- 自身为静态节点
- 内部包含子节点
- 如果内部只有一个子节点,这个节点不能是静态文本节点
可能会有人觉得突兀,前两条还好理解,为什么要加入第三条判断呢,源代码注释是这么写的:
// For a node to qualify as a static root, it should have children that
// are not just static text. Otherwise the cost of hoisting out will
// outweigh the benefits and it's better off to just always render it fresh.
是说这种节点如果被标记为静态根节点反而会影响性能,实际上原因是这个节点既然只包含一个静态文本节点,那他完全可以当作一个整体,继续向上寻找静态根节点。我们寻找静态根节点的目的,是找到最大的静态节点集合,以便整体处理这些静态节点,如果向上查找根节点的工作止步于此,反而会产生了负优化。
之后,继续对子节点、判断集合进行处理,标记所有静态根节点,优化工作就完成了。
render函数的生成
最后,我们获得了优化过的AST抽象树之后,我们就可以开始进行render函数的生成了,render函数的生成实际上就是一个递归的过程,通过对抽象树进行递归和遍历,首先生成抽象函数,再将抽象函数传入到createFunction中生成最后的render函数,抽象函数转化的一个例子:
<div id="app" v-if="a" class="app" attr="data">
2{{ msg + 1 }}1
<span v-for="item in list" :key="item.id"></span>
</div>
将会被转化为:
with(this) {
return (a) ? _c('div', {
staticClass: "app",
attrs: {
"id": "app",
"attr": "data"
}
}, [_v("\n 2" + _s(msg + 1) + "1\n "), _l((list), function (item) {
return _c('span', {
key: item.id
})
})], 2) : _e()
}
这个转化的过程主要处理了以下几件事:
- 使用_c来生成元素节点并传入相应参数
- 使用_v来生成文本节点并填入文本
- 使用_e来生成注释节点并填入注释
- 使用_m来处理静态根节点
- 使用_o来处理
v-once - 对
v-if,v-for等命令进行判断和遍历处理 - 处理
slot
代码有点多,这就不贴了。
最后,将生成的抽象函数交给createFunction,最终将抽象树编译成为这样的代码:
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createTextVNode(" 123 "),
_createElementVNode("span", null, "145"),
_createElementVNode("span", null, _toDisplayString(_ctx.num), 1 /* TEXT */)
]))
}