什么是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)[] = []// 一个数组,用于存储动静节点,最终会赋给dynamicChildrenexport let currentBlock: VNode[] | null = nullexport 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
中。