什么是Block?

Block是一种非凡的vnode,它和一般vnode相比,多出一个额定的dynamicChildren属性,用来存储动静节点。

什么是动静节点?察看上面这个vnodechildren中的第一个vnodechildren是动静的,第二个vnodeclass是动静的,这两个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-forv-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中,如果disableTrackingtrue,会将currentBlock设置为null;否则创立一个新的数组并赋值给currentBlock,并pushblockStack中。

再看createBlockcreateBlock调用一个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的返回值)被执行时,其执行流程如下:

  1. 执行_openBlock()创立一个新的数组(称其为div-block),并pushblockStack栈顶
  2. 执行_openBlock(true),因为参数为true,所以不会创立新的数组,而是将null赋值给currentBlock,并pushblockStack栈顶
  3. 执行_renderList_renderList会遍历data,并执行第二个renderItem参数,即(item) => { ... }
  4. 首先item1,执行renderItem,执行_openBlock()创立一个新的数组(称其为span1-block),并pushblockStack栈顶。此时blockStackcurrentBlock状态如下如:
  5. 接着执行_createElementBlock("span", null, _toDisplayString(item), 1 /* TEXT */),在_createElementBlock中会先调用createBaseVNode创立vnode,在创立vnode时因为这是个block vnodeisBlockNode参数为true),所以不会被收集到currentBlock
  6. 创立好vnode后,执行setupBlock,将currentBlock赋值给vnode.dynamicChildren
  7. 执行closeBlock(),弹出blcokStack的栈顶元素,并将currentBlock指向blcokStack中的最初一个元素。如下图所示:
  8. 因为此时currentBlocknull,所以跳过currentBlock.push(vnode)
  9. item = 2、item = 3时,过程与4-7步骤雷同。当item = 3时,block创立结束后的状态如下:
  10. 此时,list渲染结束,接着调用_createElementBlock(_Fragment)
  11. 执行_createElementBlock的过程中,因为isBlockNode参数为truecurrentBlocknull,所以不会被currentBlock收集
  12. 执行setupBlock,将EMPTY_ARR(空数组)赋值给vnode.dynamicChildren,并调用closeBlock(),弹出栈顶元素,使currentBlcok指向最新的栈顶元素。因为此时currentBlock不为null,所以执行currentBlock.push(vnode)
  13. 执行_createVNode(_component_ComA),创立vnode过程中,因为vnode.patchFlag === PatchFlag.PROPS,所以会将vnode增加到currentBlock中。
  14. 执行_createElementBlock('div')。先创立vnode,因为isBlockNodetrue,所以不会收集到currentBlock中。
  15. 执行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;否则如果optimizedfalse调用patchChildrenpatchChildren中可能会调用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    )  }}

总结

Blockvue3中一种性能优化的伎俩。Block实质是一种非凡的vnode,它与一般vnode相比,多出了一个dynamicChildren属性,这个属性中保留了所有Block子代的动静节点。Block进行patch能够间接对dynamicChildren中的动静节点进行patch,防止了动态节点之间的比拟。

Block的创立过程:

  1. 每次创立Block节点之前,须要调用openBlcok办法,创立一个新的数组赋值给currentBlock,并pushblockStack的栈顶。
  2. 在创立vnode的过程中如果满足一些条件,会将动静节点放到currentBlock中。
  3. 节点创立实现后,作为参数传入setupBlock中。在setupBlock中,将currentBlock复制给vnode.dynamicChildren,并调用closeBlcok,弹出blockStack栈顶元素,并使currentBlock指向最新的栈顶元素。最初如果此时currentBlock不为空,将vnode收集到currentBlock中。