什么是Block?
Block
是一种特殊的vnode
,它和普通vnode
相比,多出一个额外的dynamicChildren
属性,用来存储动态节点。
什么是动态节点?观察下面这个vnode
,children
中的第一个vnode
的children
是动态的,第二个vnode
的class
是动态的,这两个vnode
都是动态节点。动态节点都会有个patchFlag
属性,用来表示节点的什么属性时动态的。
const vnode = {
type: 'div',
children: [
{ type: 'span', children: ctx.foo, patchFlag: PatchFlags.TEXT },
{ type: 'span', children: 'foo', props: { class: normalizeClass(cls) }, patchFlag: PatchFlags.CLASS },
{ type: 'span', children: 'foo' }
]
}
作为Block
,会将其所有子代动态节点收集到dynamicChildren
中(子代的子代动态元素也会被收集到dynamicChildren
中)。
const vnode = {
type: 'div',
children: [
{ type: 'span', children: ctx.foo, patchFlag: PatchFlags.TEXT },
{ type: 'span', children: 'foo', props: { class: normalizeClass(cls) }, patchFlag: PatchFlags.CLASS },
{ type: 'span', children: 'foo' }
],
dynamicChildren: [
{ type: 'span', children: ctx.foo, patchFlag: PatchFlags.TEXT },
{ type: 'span', children: 'foo', props: { class: normalizeClass(cls) }, patchFlag: PatchFlags.CLASS }
]
}
哪些节点会作为Block?
模板中的根节点、带有v-for
、v-if/v-else-if/v-else
的节点会被作为Block
。如下示例:
dynamicChildren的收集
观察tempalte
被编译后的代码,你会发现在创建Block
之前会执行一个openBlock
函数。
// 一个block栈用于存储
export const blockStack: (VNode[] | null)[] = []
// 一个数组,用于存储动态节点,最终会赋给dynamicChildren
export let currentBlock: VNode[] | null = null
export function openBlock(disableTracking = false) {
blockStack.push((currentBlock = disableTracking ? null : []))
}
openBlock
中,如果disableTracking
为true
,会将currentBlock
设置为null
;否则创建一个新的数组并赋值给currentBlock
,并push
到blockStack
中。
再看createBlock
,createBlock
调用一个setupBlock
方法。
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
接收一个vnode
参数。
function setupBlock(vnode: VNode) {
// isBlockTreeEnabled > 0时,将currentBlock赋值给vnode.dynamicChildren
// 否则置为null
vnode.dynamicChildren =
isBlockTreeEnabled > 0 ? currentBlock || (EMPTY_ARR as any) : null
// 关闭block
closeBlock()
// 父block收集子block
// 如果isBlockTreeEnabled > 0,并且currentBlock不为null,将vnode放入currentBlock中
if (isBlockTreeEnabled > 0 && currentBlock) {
currentBlock.push(vnode)
}
// 返回vnode
return vnode
}
closeBlock
:
export function closeBlock() {
// 弹出栈顶block
blockStack.pop()
// 将currentBlock设置为父block
currentBlock = blockStack[blockStack.length - 1] || null
}
在理解dynamicChildren
的收集过程之前,我们应该先清楚对于嵌套vnode
的创建顺序是从内向外执行的。如:
export default defineComponent({
render() {
return createVNode('div', null, [
createVNode('ul', null, [
createVNode('li', null, [
createVNode('span', null, 'foo')
])
])
])
}
})
vnode
的创建过程为:span
->li
->ul
->div
。
在每次创建Block
之前,都需要调用openBlock
创建一个新数组赋值给currentBlock
,并放入blockStack
栈顶。接着调用createBlock
,在createBlock
中会先创建vnode
,并将vnode
作为参数传递给setupBlock
。
创建vnode
时,如果满足某些条件会将vnode
收集到currentBlock
中。
// 收集当前动态节点到currentBlock中
if (
isBlockTreeEnabled > 0 &&
// 避免收集自己
!isBlockNode &&
// 存在parent block
currentBlock &&
// vnode.patchFlag需要大于0或shapeFlag中存在ShapeFlags.COMPONENT
// patchFlag的存在表明该节点需要修补更新。
// 组件节点也应该总是打补丁,因为即使组件不需要更新,它也需要将实例持久化到下一个 vnode,以便以后可以正确卸载它
(vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
vnode.patchFlag !== PatchFlags.HYDRATE_EVENTS
) {
currentBlock.push(vnode)
}
接着在setupBlock
中,将currentBlock
赋值给vnode.dynamicChildren
属性,然后调用closeBlock
关闭block
(弹出blockStack
栈顶元素,并将currentBlock
执行blockStack
的最后一个元素,即刚弹出block
的父block
),接着将vnode
收集到父block
中。
示例
为了更清除dynamicChildren
的收集流程,我们通过一个例子继续进行分析。
<template>
<div>
<span v-for="item in data">{{ item }}</span>
<ComA :count="count"></ComA>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const data = reactive([1, 2, 3])
const count = ref(0)
</script>
以上示例,经过编译器编译后生成的代码如下。SFC Playground
import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, toDisplayString as _toDisplayString, resolveComponent as _resolveComponent, createVNode as _createVNode } from "vue"
import { ref, reactive } from 'vue'
const __sfc__ = {
__name: 'App',
setup(__props) {
const data = reactive([1, 2, 3])
const count = ref(0)
return (_ctx, _cache) => {
const _component_ComA = _resolveComponent("ComA")
return (_openBlock(), _createElementBlock("div", null, [
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(data, (item) => {
return (_openBlock(), _createElementBlock("span", null, _toDisplayString(item), 1 /* TEXT */))
}), 256 /* UNKEYED_FRAGMENT */)),
_createVNode(_component_ComA, { count: count.value }, null, 8 /* PROPS */, ["count"])
]))
}
}
}
__sfc__.__file = "App.vue"
export default __sfc__
当渲染函数(这里的渲染函数就是setup
的返回值)被执行时,其执行流程如下:
执行
_openBlock()
创建一个新的数组(称其为div-block
),并push
到blockStack
栈顶执行
_openBlock(true)
,由于参数为true
,所以不会创建新的数组,而是将null
赋值给currentBlock
,并push
到blockStack
栈顶执行
_renderList
,_renderList
会遍历data
,并执行第二个renderItem
参数,即(item) => { ... }
。-
首先
item
为1
,执行renderItem
,执行_openBlock()
创建一个新的数组(称其为span1-block
),并push
到blockStack
栈顶。此时blockStack
、currentBlock
状态如下如:
接着执行
_createElementBlock("span", null, _toDisplayString(item), 1 /* TEXT */)
,在_createElementBlock
中会先调用createBaseVNode
创建vnode
,在创建vnode
时因为这是个block vnode
(isBlockNode
参数为true
),所以不会被收集到currentBlock
中创建好
vnode
后,执行setupBlock
,将currentBlock
赋值给vnode.dynamicChildren
。-
执行
closeBlock()
,弹出blcokStack
的栈顶元素,并将currentBlock
指向blcokStack
中的最后一个元素。如下图所示:
由于此时
currentBlock
为null
,所以跳过currentBlock.push(vnode)
。-
item = 2、item = 3
时,过程与4-7
步骤相同。当item = 3
时,block
创建完毕后的状态如下:
此时,
list
渲染完毕,接着调用_createElementBlock(_Fragment)
。执行
_createElementBlock
的过程中,因为isBlockNode
参数为true
且currentBlock
为null
,所以不会被currentBlock
收集-
执行
setupBlock
,将EMPTY_ARR
(空数组)赋值给vnode.dynamicChildren
,并调用closeBlock()
,弹出栈顶元素,使currentBlcok
指向最新的栈顶元素。由于此时currentBlock
不为null
,所以执行currentBlock.push(vnode)
-
执行
_createVNode(_component_ComA)
,创建vnode
过程中,因为vnode.patchFlag === PatchFlag.PROPS
,所以会将vnode
添加到currentBlock
中。
执行
_createElementBlock('div')
。先创建vnode
,因为isBlockNode
为true
,所以不会收集到currentBlock
中。-
执行
setupBlock()
,将currentBlock
赋给vnode.dynamicChildren
。然后执行closeBlock()
,弹出栈顶元素,此时blockStack
长度为0,所以currentBlock
会指向null
最终生成的vnode
:
{
type: "div",
children:
[
{
type: Fragment,
children: [{
type: "span",
children: "1",
patchFlag: PatchFlag.TEXT,
dynamicChildren: [],
},
{
type: "span",
children: "2",
patchFlag: PatchFlag.TEXT,
dynamicChildren: [],
},
{
type: "span",
children: "3",
patchFlag: PatchFlag.TEXT,
dynamicChildren: [],
}],
patchFlag: PatchFlag.UNKEYED_FRAGMENT,
dynamicChildren: []
},
{
type: ComA,
children: null,
patchFlag: PatchFlag.PROPS,
dynamicChildren: null
}
]
,
patchFlag:0,
dynamicChildren: [
{
type: Fragment,
children: [{
type: "span",
children: "1",
patchFlag: PatchFlag.TEXT,
dynamicChildren: [],
},
{
type: "span",
children: "2",
patchFlag: PatchFlag.TEXT,
dynamicChildren: [],
},
{
type: "span",
children: "3",
patchFlag: PatchFlag.TEXT,
dynamicChildren: [],
}],
patchFlag: PatchFlag.UNKEYED_FRAGMENT,
dynamicChildren: []
},
{
type: ComA,
children: null,
patchFlag: PatchFlag.PROPS,
dynamicChildren: null
}
]
}
Block的作用
如果你了解Diff过程,你应该知道在Diff
过程中,即使vnode
没有发生变化,也会进行一次比较。而Block
的出现减少了这种不必要的的比较,由于Block
中的动态节点都会被收集到dynamicChildren
中,所以Block
间的patch
可以直接比较dynamicChildren
中的节点,减少了非动态节点之间的比较。
Block
之间进行patch
时,会调用一个patchBlockChildren
方法来对dynamicChildren
进行patch
。
const patchElement = (
n1: VNode,
n2: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
// ...
let { patchFlag, dynamicChildren, dirs } = n2
if (dynamicChildren) {
patchBlockChildren(
n1.dynamicChildren!,
dynamicChildren,
el,
parentComponent,
parentSuspense,
areChildrenSVG,
slotScopeIds
)
if (__DEV__ && parentComponent && parentComponent.type.__hmrId) {
traverseStaticChildren(n1, n2)
}
} else if (!optimized) {
patchChildren(
n1,
n2,
el,
null,
parentComponent,
parentSuspense,
areChildrenSVG,
slotScopeIds,
false
)
}
// ...
}
patchElement
中如果新节点存在dynamicChildren
,说明此时新节点是个Block
,那么会调用patchBlockChildren
方法对dynamicChildren
进行patch
;否则如果optimized
为false
调用patchChildren
,patchChildren
中可能会调用patchKeyedChildren/patchUnkeyedChildren
进行Diff
。
const patchBlockChildren: PatchBlockChildrenFn = (
oldChildren,
newChildren,
fallbackContainer,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds
) => {
for (let i = 0; i < newChildren.length; i++) {
const oldVNode = oldChildren[i]
const newVNode = newChildren[i]
// 确定父容器
const container =
oldVNode.el &&
(oldVNode.type === Fragment ||
!isSameVNodeType(oldVNode, newVNode) ||
oldVNode.shapeFlag & (ShapeFlags.COMPONENT | ShapeFlags.TELEPORT))
? hostParentNode(oldVNode.el)!
: fallbackContainer
patch(
oldVNode,
newVNode,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
true
)
}
}
总结
Block
是vue3
中一种性能优化的手段。Block
本质是一种特殊的vnode
,它与普通vnode
相比,多出了一个dynamicChildren
属性,这个属性中保存了所有Block
子代的动态节点。Block
进行patch
可以直接对dynamicChildren
中的动态节点进行patch
,避免了静态节点之间的比较。
Block
的创建过程:
- 每次创建
Block
节点之前,需要调用openBlcok
方法,创建一个新的数组赋值给currentBlock
,并push
到blockStack
的栈顶。 - 在创建
vnode
的过程中如果满足一些条件,会将动态节点放到currentBlock
中。 - 节点创建完成后,作为参数传入
setupBlock
中。在setupBlock
中,将currentBlock
复制给vnode.dynamicChildren
,并调用closeBlcok
,弹出blockStack
栈顶元素,并使currentBlock
指向最新的栈顶元素。最后如果此时currentBlock
不为空,将vnode
收集到currentBlock
中。