前言
原文再续,书接上一回,上次讲到parse函数把模板编译成了 AST,这里就来讲optimize发生的事情。
graph LR A[template 模板]-->|parse|B[AST 抽象语法树]-->|optimize|C[优化后的 AST]-->|generate|D[render]-->|return|E[虚拟 DOM]
静态节点
动态节点其实就是不是静态节点的节点,会随着绑定的变量的变化而改变,例如:
- <div :class="active ? 'active' : ''"></div>
active从true变成false了,它的类名就变化了。
众所周知背过两句八股文就可以发现,Vue 3 对静态节点有静态提升的操作。什么是静态节点呢,就是不动态的节点,可以大致理解为没有v-bind:xxx、v-on:xxx、v-if什么的 Vue 中进行属性绑定的属性的节点,这样子的节点在不管页面怎么变化它都不会变。例如:
ts- <div>
- <span>static</span>
- </div>
div和span都是静态节点,div子节点全是静态节点,那它就是静态根节点
optimize做的事情很简单,遍历 AST 树,标记出所有静态节点和静态根节点,供后面generate使用。
流程总览
流程很简单:
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标记静态节点,markStaticRoots标记静态根节点。
静态节点
markStatic从根节点开始递归,一个节点是静态的需要满足:自己是静态的,所有子节点是静态的,同一层次的 if 分支都是静态的:
ts- function markStatic(node: ASTNode) {
- node.static = isStatic(node)
- if (node.type === 1) {
- if (
- !isPlatformReservedTag(node.tag) &&
- node.tag !== 'slot' &&
- node.attrsMap['inline-template'] == null
- ) {
- return
- }
- // ...
- }
- }
isStatic判断自身是不是静态的:
ts- 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))
- )
- }
node.type === 2表示是动态文本,例如:num = {{count}},这是动态的。node.type === 3是静态的普通文本,当然是静态的。为 1 就是标签了。
一个节点是静态的需要满足:
node.pre,这个指令可以跳过该元素及其所有子元素的编译,让标签及其内容视为纯文本,当然是静态的。- 否则需要满足:
2.1.!node.hasBindings,没有动态绑定,没有v-xx、@xx、:xx的属性。
2.2.!node.if,没有v-if。
2.3.!node.for,没有v-for。
2.4.!isBuiltInTag(node.tag),不是 Vue 内部的标签:slot、component等等。
2.5.isPlatformReservedTag(node.tag),是平台提供的标签,浏览器上就是原生 HTML 元素。
2.6.!isDirectChildOfTemplateFor(node),不是<template v-if="xxx">...</template>的子标签。
2.7.Object.keys(node).every(isStaticKey)),没有非静态属性,静态属性只有:type、tag、attrsList、attrsMap、plain、parent、children、attrs、start、end、rawAttrsMap,还有staticStyle和staticClass,这两个属性是parse的时候收集的标签上面静态的class和style。
一个节点是静态节点,不仅要看它自己是不是静态的,还要看子节点的情况。
ts- function markStatic(node: ASTNode) {
- 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
- }
- }
- // ...
- }
- }
深度遍历子节点,如果有一个子节点不是静态的,当前节点node.static = false,也就是说,当前节点是不是静态节点,是当前节点和子节点的是不是静态的的交。
下面还考虑到 if 的分支:
ts- function markStatic(node: ASTNode) {
- node.static = isStatic(node)
- if (node.type === 1) {
- // ...
- 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
- }
- }
- }
- }
- }
之前文章没有说,parse处理v-if系列时,会给节点加上这样子的ifConditions属性:
ts- type ifConditions = ASTIfCondition[]
- type ASTIfCondition = {
- exp: string | undefined; // v-if,以及后续 v-else-if,v-else 的表达式
- block: ASTElement; // v-if,以及后续 v-else-if,v-else 的各分支节点
- }
也就是说,如果节点有v-if的话,需要继续遍历完它的各同层次分支,因为这些分支不在它的子节点上面(Q:但是 if 各分支不是是根节点的子节点吗?A:v-else-if和v-else分支不会被添加到亲节点上)。
静态根节点
静态根节点与后续静态提升有关,逻辑也是遍历 AST 树,判断每个节点是不是静态根节点:
ts- function markStaticRoots(node: ASTNode, isInFor: boolean) {
- if (node.type === 1) {
- if (node.static || node.once) {
- node.staticInFor = isInFor
- }
- // ...
- }
- }
staticInFor用于标记 for 循环渲染中的静态节点,once表示标签有v-once指令,说明这个标签仅仅渲染一次,以后数据发生更新也不重新渲染。
静态根节点要求节点是静态的,并且有非纯文本的子元素:
ts- 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
- }
- // ...
- }
- }
为什么要非纯文本子元素,Vue 解释说纯文本或者没有子元素的标签静态提升对性能提高不大,没必要进行静态提升。
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.
接下来还是深度遍历子元素和 if 分支的逻辑:
ts- function markStaticRoots(node: ASTNode, isInFor: boolean) {
- // ...
- if (node.type === 1) {
- // ...
- 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)
- }
- }
- }
- }
总结
optimize,遍历 AST 树,标记出所有静态节点和静态根节点,供后面generate使用,其实也是为提取dynamicChildren和静态节点的提升做准备。
