前言
Vue 3 中,使用 Block 对渲染进行了优化,某些 VNode 中有dynamicChildren
属性,它收集了动态子代节点的 VNode,当存在dynamicChildren
时,diff 的时候只需要比较dynamicChildren
中的 VNode, 相比前代的全量比较,提高了性能。
render 函数
模板编译部分就不看了,分支太多了,总之,Vue 会为组件根节点(多节点则自动创建template
作为根节点)、v-if
、v-for
、SVG 节点等情况创建 Block。举个栗子🌰:
html- <div>
- <div>TEST</div>
- <div>{{data}}</div>
- <div :data="test">TEMP</div>
- <div v-for="item in arr">{{item}}</div>
- <div v-if="data">IF</div>
- <div v-else-if="count">ELIF</div>
- <div v-else>ELSE</div>
- </div>
编译后:
ts- const _Vue = Vue
- const { createElementVNode: _createElementVNode, createCommentVNode: _createCommentVNode } = _Vue
- const _hoisted_1 = /*#__PURE__*/_createElementVNode("div", null, "TEST", -1 /* HOISTED */)
- const _hoisted_2 = ["data"]
- const _hoisted_3 = { key: 0 }
- const _hoisted_4 = { key: 1 }
- const _hoisted_5 = { key: 2 }
- return function render(_ctx, _cache) {
- with (_ctx) {
- const { createElementVNode: _createElementVNode, toDisplayString: _toDisplayString, renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode } = _Vue
- return (_openBlock(), _createElementBlock("div", null, [
- _hoisted_1,
- _createElementVNode("div", null, _toDisplayString(data), 1 /* TEXT */),
- _createElementVNode("div", { data: test }, "TEMP", 8 /* PROPS */, _hoisted_2),
- (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(arr, (item) => {
- return (_openBlock(), _createElementBlock("div", null, _toDisplayString(item), 1 /* TEXT */))
- }), 256 /* UNKEYED_FRAGMENT */)),
- data
- ? (_openBlock(), _createElementBlock("div", _hoisted_3, "IF"))
- : count
- ? (_openBlock(), _createElementBlock("div", _hoisted_4, "ELIF"))
- : (_openBlock(), _createElementBlock("div", _hoisted_5, "ELSE"))
- ]))
- }
- }
我们可以看到根节点,v-if
系列、v-for
都使用_openBlock
来创建 Block,_createElementBlock
进行后续的处理。
创建 Block
来看openBlock
的代码:
ts- export const blockStack: (VNode[] | null)[] = []
- export let currentBlock: VNode[] | null = null
- export function openBlock(disableTracking = false) {
- blockStack.push((currentBlock = disableTracking ? null : []))
- }
Block 是一个栈结构,可以实现 Block 嵌套的功能。创建了一个 Block,栈中压入一个数组;收集节点时,节点被添加到数组末尾;收集完节点后,将当前 Block 数组出栈,并且加入上一级的 Block 数组中。另外,根据dynamicChildren
的逻辑,diff 需要前后子代节点顺序一致。遇到可能不一致的地方,Vue 可以再加入一个 Block,把 Block 的根节点插入上级 Block 中,对于子 Block 中不稳定的子节点结构,不收集dynamicChildren
,diff 时降级依旧使用全量对比。这样子可以避免比对时子代节点顺序不一致的问题。
收集动态子代节点
再来看 VNode,根节点的dynamicChildren
有 4 个元素,分别是<div>{{data}}</div>
、<div :data="test">TEMP</div>
两个动态节点,以及v-for
的根节点和v-if
3 个分支中,其中一个的根节点。静态节点是不在里面的。
在编译后的 render 函数里面:
ts- (_openBlock(), _createElementBlock("div", null, [
- _hoisted_1,
- _createElementVNode("div", null, _toDisplayString(data), 1 /* TEXT */),
- _createElementVNode("div", { data: test }, "TEMP", 8 /* PROPS */, _hoisted_2),
- (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(arr, (item) => {
- return (_openBlock(), _createElementBlock("div", null, _toDisplayString(item), 1 /* TEXT */))
- }), 256 /* UNKEYED_FRAGMENT */)),
- data
- ? (_openBlock(), _createElementBlock("div", _hoisted_3, "IF"))
- : count
- ? (_openBlock(), _createElementBlock("div", _hoisted_4, "ELIF"))
- : (_openBlock(), _createElementBlock("div", _hoisted_5, "ELSE"))
- ]))
可以看见,创建根节点时,_createElementBlock
的第 3 个入参,是创建好了的子节点数组,很显然,创建子节点是依据 DFS 从下到上,从前到后的顺序进行的。_createElementBlock
中有关闭 Block 的逻辑,这样子就确保了,关闭 Block 的时候 Block 已经收集完子代节点了。
createBaseVNode
用于创建标签、组件、文本等类型的 VNode。节点是否加入 Block 的相关逻辑如下:
ts- function createBaseVNode(
- type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
- props: (Data & VNodeProps) | null = null,
- children: unknown = null,
- patchFlag = 0,
- dynamicProps: string[] | null = null,
- shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,
- isBlockNode = false,
- needFullChildrenNormalization = false,
- ) {
- // ...
- // track vnode for block tree
- if (
- isBlockTreeEnabled > 0 &&
- // avoid a block node from tracking itself
- !isBlockNode &&
- // has current parent block
- currentBlock &&
- // presence of a patch flag indicates this node needs patching on updates.
- // component nodes also should always be patched, because even if the
- // component doesn't need to update, it needs to persist the instance on to
- // the next vnode so that it can be properly unmounted later.
- (vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
- // the EVENTS flag is only for hydration and if it is the only flag, the
- // vnode should not be considered dynamic due to handler caching.
- vnode.patchFlag !== PatchFlags.NEED_HYDRATION
- ) {
- currentBlock.push(vnode)
- }
- // ...
- return vnode
- }
在这里可以看到,vnode.patchFlag > 0
才会被收集,说明动态的 VNode 才会被收集。patchFlag
不大于 0 有两种情况:-1:被提升的静态节点;-2:一些特殊节点,例如手写 JSX,它不会使用 Block 的优化逻辑。
而且,这里的currentBlock
会收集子代的节点,而不是只有子节点。试想一下,组件根节点创建了 Block,而其孙节点是动态的,之间没有创建 Block 的子节点,这样子,孙节点也会被组件根节点的currentBlock
收集。
还有一些特殊情况:
ts- function _createVNode(
- type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
- props: (Data & VNodeProps) | null = null,
- children: unknown = null,
- patchFlag: number = 0,
- dynamicProps: string[] | null = null,
- isBlockNode = false,
- ): VNode {
- // ...
- if (isVNode(type)) {
- // createVNode receiving an existing vnode. This happens in cases like
- // <component :is="vnode"/>
- const cloned = cloneVNode(type, props, true /* mergeRef: true */)
- if (children) {
- normalizeChildren(cloned, children)
- }
- if (isBlockTreeEnabled > 0 && !isBlockNode && currentBlock) {
- if (cloned.shapeFlag & ShapeFlags.COMPONENT) {
- // 没太看懂为什么要怎么做
- currentBlock[currentBlock.indexOf(type)] = cloned
- } else {
- currentBlock.push(cloned)
- }
- }
- cloned.patchFlag |= PatchFlags.BAIL
- return cloned
- }
- // ...
- return createBaseVNode(
- type,
- props,
- children,
- patchFlag,
- dynamicProps,
- shapeFlag,
- isBlockNode,
- true,
- )
- }
这里是给component
标签传入一个 VNode 实例的情况,感觉情况很少见。
关闭 Block
来看_createElementBlock
,也就是createElementBlock
:
ts- export function createElementBlock(
- type: string | typeof Fragment,
- props?: Record<string, any> | null,
- children?: any,
- patchFlag?: number,
- dynamicProps?: string[],
- shapeFlag?: number,
- ) {
- return setupBlock(
- createBaseVNode(
- type,
- props,
- children,
- patchFlag,
- dynamicProps,
- shapeFlag,
- true /* isBlock */,
- ),
- )
- }
给createBaseVNode
传入创建好的 Block 根节点 VNode。类似地,还有函数createBlock
,这个常用于template
、component
这样子的特殊标签。
ts- export function createBlock(
- type: VNodeTypes | ClassComponent,
- props?: Record<string, any> | null,
- children?: any,
- patchFlag?: number,
- dynamicProps?: string[],
- ): VNode {
- return setupBlock(
- createVNode(
- type,
- props,
- children,
- patchFlag,
- dynamicProps,
- true /* isBlock: prevent a block from tracking itself */,
- ),
- )
- }
setupBlock
用于使当前 Block 出栈,并且记录dynamicChildren
,并且把当前 Block 根节点记录到上一级的 Block 中:
ts- export function closeBlock() {
- blockStack.pop()
- currentBlock = blockStack[blockStack.length - 1] || null
- }
- function setupBlock(vnode: VNode) {
- // save current block children on the block vnode
- vnode.dynamicChildren =
- isBlockTreeEnabled > 0 ? currentBlock || (EMPTY_ARR as any) : null
- // close block
- closeBlock()
- // a block is always going to be patched, so track it as a child of its
- // parent block
- if (isBlockTreeEnabled > 0 && currentBlock) {
- currentBlock.push(vnode)
- }
- return vnode
- }
结语
这里描述了 Vue 3 渲染优化中的 Block 创建流程。Block 通过 DFS 的顺序,收集同一个 Block 下的子代动态节点,达到 diff 的时候减少比对的目的。对于可能不稳定的结构,通过新加入新 Block 来使上层 Block 的 VNode 有一个稳定的顺序。