什么是 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
。如下示例:
SFC Playground
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
中。