什么是静态提升
静态提升是Vue3
编译优化中的其中一个优化点。所谓的静态提升 ,就是指在编译器编译的过程中,将一些静态的节点或属性提升到渲染函数之外。下面,我们通过一个例子来深入理解什么是静态提升。
假设我们有如下模板:
html
<div>
<p>static text</p>
<p>{{ title }}</p>
</div>
在没有静态提升的情况下,它对应的渲染函数是:
js
function render() {
return (openClock(), createClock('div', null, [
createVNode('p', null, 'static text'),
createVNode('p', null, ctx.title, 1 /* TEXT */)
]))
}
从上面的代码中可以看到,在这段虚拟DOM的描述中存在两个 p 标签,一个是纯静态的,而另一个拥有动态文本。当响应式数据 title
的值发生变化时,整个渲染函数会重新执行,并产生新的虚拟DOM树。在这个过程中存在性能开销的问题。原因是纯静态的虚拟节点在更新时也会被重新创建一次,其实这是没有必要的。因此我们需要使用静态提升来解决这个问题。
所谓的 "静态提升",就是将一些静态的节点或属性
提升到渲染函数
之外。就像下面的代码所示:
js
// 把静态节点提升到渲染函数之外
const hoist1 = createVNode('p', null, 'text')
function render() {
return (openBlock(), createBlock('div', null, [
hoist1, // 静态节点引用
createVNode('p', null, ctx.title, 1 /* TEXT */)
]))
}
可以看到,经过静态提升后,在渲染函数内只会持有对静态节点的引用。当响应式数据发生变化,并使得渲染函数重新执行时,并不会重新创建静态的虚拟节点,从而避免了额外的性能开销。
transform 转换器
在模板编译器将模板编译为渲染函数的过程中,transform
函数扮演着十分重要的角色。它用来将模板AST转换为 JavaScript AST
。下面,我们来看看 transform
函数做了什么。
js
// packages/compiler-core/src/transform.ts
// 将 模板AST 转换为 JavaScript AST
export function transform(root: RootNode, options: TransformOptions) {
// 创建转换上下文
const context = createTransformContext(root, options)
// 遍历所有节点,执行转换
traverseNode(root, context)
// 如果编译选项中打开了 hoistStatic 选项,则进行静态提升
if (options.hoistStatic) {
hoistStatic(root, context)
}
// 创建 Block,收集所有动态子节点
if (!options.ssr) {
createRootCodegen(root, context)
}
// finalize meta information
// 确定最终的元信息
root.helpers = [...context.helpers.keys()]
root.components = [...context.components]
root.directives = [...context.directives]
root.imports = context.imports
root.hoists = context.hoists
root.temps = context.temps
root.cached = context.cached
if (__COMPAT__) {
root.filters = [...context.filters!]
}
}
从上面的源码中可以看到,transform
函数的实现并不复杂。
- 首先,调用
createTransformContext
函数创建了一个转换上下文对象,该对象存储着转换过程中的一些上下文数据。例如当前正在转换的节点currentNode
、当前转换节点的父节点parent、用于替换当前正在转换的节点的 replaceNode 函数、用于移除当前访问的节点的removeNode
函数等。 - 接着调用
traverseNode
函数,递归遍历模板AST,将模板AST 转换为JavaScript AST
。 - 然后判断编译选项options中是否开启了
hoistStatic
,如果是,则进行静态提升。 - 接下来则创建
Block
,收集模板中的动态子节点。 - 最后做的事情则是确定最终的元信息。
由于本文主要是介绍静态提升 ,因此我们围绕静态提升的代码继续往下探索,其余部分代码将在其它文章中介绍。
hoistStatic 静态提升
hoistStatic
函数的源码如下所示:
js
// packages/compiler-core/src/transforms/hoistStatic.ts
export function hoistStatic(root: RootNode, context: TransformContext) {
walk(
root,
context,
// Root node is unfortunately non-hoistable due to potential parent
// fallthrough attributes.
// 根节点作为 Block 角色是不能被提升的
isSingleElementRoot(root, root.children[0])
)
}
可以看到,hoistStatic
函数接收两个参数,第一个参数是根节点,第二个参数是转换上下文。在该函数中,仅仅只是调用了 walk
函数来实现静态提升。并且在 walk
函数中调用 isSingleElementRoot
函数来告知 walk
函数根节点是不能提升的,原因是根节点作为 Block
角色是不可被提升的。
我们接下来继续探究 walk
函数的源码。
walk 函数
静态提升的真正实现逻辑在 walk
函数内,其源码如下所示:
js
function walk(
node: ParentNode,
context: TransformContext, // 转换上下文对象
doNotHoistNode: boolean = false // 节点是否可以被提升
) {
const { children } = node
// 子节点的数量
const originalCount = children.length
// 可提升节点的数量
let hoistedCount = 0
for (let i = 0; i < children.length; i++) {
const child = children[i]
// 只有普通元素和文本才能被提升
if (
child.type === NodeTypes.ELEMENT &&
child.tagType === ElementTypes.ELEMENT
) {
// 如果节点不能被提升,则将 constantType 赋值为 NOT_CONSTANT 不可被提升的标记
// 否则调用 getConstantType 获取子节点的静态类型:ConstantTypes 定义了子节点的静态类型
const constantType = doNotHoistNode
? ConstantTypes.NOT_CONSTANT
: getConstantType(child, context)
// 如果获取到的 constantType 枚举值大于 NOT_CONSTANT
if (constantType > ConstantTypes.NOT_CONSTANT) {
// 如果节点可以被提升
if (constantType >= ConstantTypes.CAN_HOIST) {
// 则将子节点的 codegenNode 属性的 patchFlag 标记为 HOISTED ,即可提升
;(child.codegenNode as VNodeCall).patchFlag =
PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
// 提升节点,将节点存储到 转换上下文context 的 hoist 数组中
child.codegenNode = context.hoist(child.codegenNode!)
// 提升节点数量自增1
hoistedCount++
continue
}
} else {
// 包含动态绑定的节点本身不会被提升,该动态节点上可能存在纯静态的属性,静态的属性可以被提升
const codegenNode = child.codegenNode!
if (codegenNode.type === NodeTypes.VNODE_CALL) {
// 获取 patchFlag 补丁标志
const flag = getPatchFlag(codegenNode)
// 如果不存在 patchFlag 补丁标志 或者 patchFlag 是文本类型
// 并且该节点的 props 是可以被提升的
if (
(!flag ||
flag === PatchFlags.NEED_PATCH ||
flag === PatchFlags.TEXT) &&
getGeneratedPropsConstantType(child, context) >=
ConstantTypes.CAN_HOIST
) {
// 获取节点的 props,并在转换上下文对象中执行提升操作,
// 将被提升的 props 添加到转换上下文context 的 hoist 数组中
const props = getNodeProps(child)
if (props) {
codegenNode.props = context.hoist(props)
}
}
// 将节点的动态 props 添加到转换上下文对象中
if (codegenNode.dynamicProps) {
codegenNode.dynamicProps = context.hoist(codegenNode.dynamicProps)
}
}
}
} else if (
// 如果是节点类型是 TEXT_CALL,并且节点可以被提升
child.type === NodeTypes.TEXT_CALL &&
getConstantType(child.content, context) >= ConstantTypes.CAN_HOIST
) {
// 提升节点,将节点存储到 转换上下文context 的 hoist 数组中
child.codegenNode = context.hoist(child.codegenNode)
hoistedCount++
}
// walk further
if (child.type === NodeTypes.ELEMENT) {
// 如果子节点的 tagType 是组件,则继续遍历子节点
// 以判断插槽中的情况
const isComponent = child.tagType === ElementTypes.COMPONENT
if (isComponent) {
context.scopes.vSlot++
}
// 执行 walk函数,继续判断插槽中的节点及节点属性是否可以被提升
walk(child, context)
if (isComponent) {
context.scopes.vSlot--
}
} else if (child.type === NodeTypes.FOR) {
// 带有 v-for 指令的节点是一个 Block
// 如果 v-for 的节点中只有一个子节点,则不能被提升
walk(child, context, child.children.length === 1)
} else if (child.type === NodeTypes.IF) {
// 带有 v-if 指令的节点是一个 Block
for (let i = 0; i < child.branches.length; i++) {
// 如果只有一个分支条件,则不进行提升
walk(
child.branches[i],
context,
child.branches[i].children.length === 1
)
}
}
}
// 将被提升的节点序列化,即转换成字符串
if (hoistedCount && context.transformHoist) {
context.transformHoist(children, context, node)
}
// 判断是否存在可提升的子节点,并且所有子节点都已经被提升。如果是上述情况之一,则可以将整个子节点数组提升到父级作用域中
if (
hoistedCount &&
hoistedCount === originalCount &&
node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.ELEMENT &&
node.codegenNode &&
node.codegenNode.type === NodeTypes.VNODE_CALL &&
isArray(node.codegenNode.children)
) {
// 将子节点数组转换为数组表达式,并将其提升到父级作用域中
node.codegenNode.children = context.hoist(
createArrayExpression(node.codegenNode.children)
)
}
}
枚举ConstantTypes
的定义如下:
js
/**
* 静态类型的级别是有顺序的,即高级别的类型包含低级别的类型。例如,可以转换为字符串的节点一定可以进行提升和跳过补丁
*/
export const enum ConstantTypes {
NOT_CONSTANT = 0,
CAN_SKIP_PATCH,
CAN_HOIST,
CAN_STRINGIFY
}
可以看到,walk
函数的代码比较长,下面,我们将分步对其进行解析。
1、首先,我们来看 walk
函数的函数签名,代码如下:
js
function walk(
node: ParentNode,
context: TransformContext, // 转换上下文对象
doNotHoistNode: boolean = false // 节点是否可以被提升
)
从函数签名中可以知道,walk
函数接收三个参数,第一个参数是一个 node
节点,第二个参数是转换器的转换上下文对象 context
,第三个参数 doNotHoistNode
是一个布尔值,用来判断传入节点的子节点是否可以被提升。
2、初始化两个变量: - originalCount
:子节点的数量; - hoistedCount
:可提升节点的数量,该变量将会用于判断被提升的节点是否可序列化。
js
const { children } = node
// 子节点的数量
const originalCount = children.length
// 可提升节点的数量
let hoistedCount = 0
3、我们来看 for
循环语句里的第一个 if
语句分支,这里处理的是普通元素和静态文本被提升的情况。
js
// 只有普通元素和文本才能被提升
if (
child.type === NodeTypes.ELEMENT &&
child.tagType === ElementTypes.ELEMENT
) {
// 如果节点不能被提升,则将 constantType 赋值为 NOT_CONSTANT 不可被提升的标记
// 否则调用 getConstantType 获取子节点的静态类型:ConstantTypes 定义了子节点的静态类型
const constantType = doNotHoistNode
? ConstantTypes.NOT_CONSTANT
: getConstantType(child, context)
// 如果获取到的 constantType 枚举值大于 NOT_CONSTANT
if (constantType > ConstantTypes.NOT_CONSTANT) {
// 如果节点可以被提升
if (constantType >= ConstantTypes.CAN_HOIST) {
// 则将子节点的 codegenNode 属性的 patchFlag 标记为 HOISTED ,即可提升
;(child.codegenNode as VNodeCall).patchFlag =
PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
// 提升节点,将节点存储到 转换上下文context 的 hoist 数组中
child.codegenNode = context.hoist(child.codegenNode!)
// 提升节点数量自增1
hoistedCount++
continue
}
} else {
// 包含动态绑定的节点本身不会被提升,该动态节点上可能存在纯静态的属性,静态的属性可以被提升
const codegenNode = child.codegenNode!
if (codegenNode.type === NodeTypes.VNODE_CALL) {
// 获取 patchFlag 补丁标志
const flag = getPatchFlag(codegenNode)
// 如果不存在 patchFlag 补丁标志 或者 patchFlag 是文本类型
// 并且该节点的 props 是可以被提升的
if (
(!flag ||
flag === PatchFlags.NEED_PATCH ||
flag === PatchFlags.TEXT) &&
getGeneratedPropsConstantType(child, context) >=
ConstantTypes.CAN_HOIST
) {
// 获取节点的 props,并在转换上下文对象中执行提升操作,
// 将被提升的 props 添加到转换上下文context 的 hoist 数组中
const props = getNodeProps(child)
if (props) {
codegenNode.props = context.hoist(props)
}
}
// 将节点的动态 props 添加到转换上下文对象中
if (codegenNode.dynamicProps) {
codegenNode.dynamicProps = context.hoist(codegenNode.dynamicProps)
}
}
}
首先通过外部传入的 doNotHoistNode
参数来获取子节点的静态类型。如果 doNotHoistNode
为 true,则将 constantType
的值设置为 ConstantType
枚举值中的 NOT_CONSTANT
,即不可被提升。否则通过 getConstantType
函数获取子节点的静态类型。如下面的代码所示:
js
// 如果节点不能被提升,则将 constantType 赋值为 NOT_CONSTANT 不可被提升的标记
// 否则调用 getConstantType 获取子节点的静态类型:ConstantTypes 定义了子节点的静态类型
const constantType = doNotHoistNode
? ConstantTypes.NOT_CONSTANT
: getConstantType(child, context)
接下来通过判断 constantType
的枚举值来处理是需要提升静态节点 还是提升动态节点的静态属性 。如果获取到的 constantType
枚举值大于 NOT_CONSTANT
,说明该节点可能被提升或序列化为字符串。如果该节点可以被提升,则将节点 codegenNode
属性的 patchFlag
标记为 PatchFlags.HOISTED
,即可提升。然后执行转换器上下文中的 hoist
方法,将该节点存储到转换上下文 context
的 hoist
数组中,该数组中存储的是可被提升的节点。如下面的代码所示:
js
// 如果获取到的 constantType 枚举值大于 NOT_CONSTANT
if (constantType > ConstantTypes.NOT_CONSTANT) {
// 如果节点可以被提升
if (constantType >= ConstantTypes.CAN_HOIST) {
// 则将子节点的 codegenNode 属性的 patchFlag 标记为 HOISTED ,即可提升
;(child.codegenNode as VNodeCall).patchFlag =
PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
// 提升节点,将节点存储到 转换上下文context 的 hoist 数组中
child.codegenNode = context.hoist(child.codegenNode!)
// 提升节点数量自增1
hoistedCount++
continue
}
}
如果获取到的 constantType
枚举值不大于 NOT_CONSTANT
,说明该节点包含动态绑定,包含动态绑定的节点上如果存在纯静态的 props,那么这些静态的 props 是可以被提升的 。从下面的代码中我们可以看到,在提升静态的 props
时,同样是执行转换器上下文中的 hoist
方法,将静态的props
存储到转换上下文 context
的 hoist
数组中。如下面的代码所示:
js
else {
// 包含动态绑定的节点本身不会被提升,该动态节点上可能存在纯静态的属性,静态的属性可以被提升
const codegenNode = child.codegenNode!
if (codegenNode.type === NodeTypes.VNODE_CALL) {
// 获取 patchFlag 补丁标志
const flag = getPatchFlag(codegenNode)
// 如果不存在 patchFlag 补丁标志 或者 patchFlag 是文本类型
// 并且该节点的 props 是可以被提升的
if (
(!flag ||
flag === PatchFlags.NEED_PATCH ||
flag === PatchFlags.TEXT) &&
getGeneratedPropsConstantType(child, context) >=
ConstantTypes.CAN_HOIST
) {
// 获取节点的 props,并在转换上下文对象中执行提升操作,
// 将被提升的 props 添加到转换上下文context 的 hoist 数组中
const props = getNodeProps(child)
if (props) {
codegenNode.props = context.hoist(props)
}
}
// 将节点的动态 props 添加到转换上下文对象中
if (codegenNode.dynamicProps) {
codegenNode.dynamicProps = context.hoist(codegenNode.dynamicProps)
}
}
}
4、如果节点类型时是 TEXT_CALL
类型并且该节点可以被提升,则同样执行转换器上下文中的 hoist
方法,将节点存储到转换上下文context
的 hoist
数组中,如下面的代码所示:
js
else if (
// 如果是节点类型是 TEXT_CALL,并且节点可以被提升
child.type === NodeTypes.TEXT_CALL &&
getConstantType(child.content, context) >= ConstantTypes.CAN_HOIST
) {
// 提升节点
child.codegenNode = context.hoist(child.codegenNode)
hoistedCount++
}
5、如果子节点是组件,则递归调用 walk 函数,继续遍历子节点,以判断插槽中的节点及节点属性是否可以被提升。如下面的代码所示:
js
if (child.type === NodeTypes.ELEMENT) {
// 如果子节点的 tagType 是组件,则继续遍历子节点
// 以判断插槽中的情况
const isComponent = child.tagType === ElementTypes.COMPONENT
if (isComponent) {
context.scopes.vSlot++
}
// 执行 walk函数,继续判断插槽中的节点及节点属性是否可以被提升
walk(child, context)
if (isComponent) {
context.scopes.vSlot--
}
}
6、如果节点上带有 v-for
指令或 v-if
指令,则递归调用 walk
函数,继续判断子节点是否可以被提升。如果 v-for
指令的节点只有一个子节点,v-if
指令的节点只有一个分支条件,则不进行提升。如下面的代码所示:
js
else if (child.type === NodeTypes.FOR) {
// Do not hoist v-for single child because it has to be a block
// 带有 v-for 指令的节点是一个 Block
// 如果 v-for 的节点中只有一个子节点,则不能被提升
walk(child, context, child.children.length === 1)
} else if (child.type === NodeTypes.IF) {
// 带有 v-if 指令的节点是一个 Block
for (let i = 0; i < child.branches.length; i++) {
// Do not hoist v-if single child because it has to be a block
// 如果只有一个分支条件,则不进行提升
walk(
child.branches[i],
context,
child.branches[i].children.length === 1
)
}
}
walk
函数流程图
总结
静态提升,就是指在编译器编译的过程中,将一些静态的节点或属性提升到渲染函数之外 。它能够减少更新时创建虚拟 DOM
带来的性能开销和内存占用。
对于纯静态的节点和动态节点上的纯静态属性 ,则直接执行转换器上下文中的 hoist
方法,将节点或属性进行提升。如果节点是组件、节点上带有 v-for
指令或 v-if
指令,则递归调用 walk
函数判断子节点是否可以被提升。