likes
comments
collection
share

【vue3源码】十三、认识Block

作者站长头像
站长
· 阅读数 34

什么是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

【vue3源码】十三、认识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中,如果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状态如下如:【vue3源码】十三、认识Block
  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中的最后一个元素。如下图所示:【vue3源码】十三、认识Block
  8. 由于此时currentBlocknull,所以跳过currentBlock.push(vnode)
  9. item = 2、item = 3时,过程与4-7步骤相同。当item = 3时,block创建完毕后的状态如下:【vue3源码】十三、认识Block
  10. 此时,list渲染完毕,接着调用_createElementBlock(_Fragment)
  11. 执行_createElementBlock的过程中,因为isBlockNode参数为truecurrentBlocknull,所以不会被currentBlock收集
  12. 执行setupBlock,将EMPTY_ARR(空数组)赋值给vnode.dynamicChildren,并调用closeBlock(),弹出栈顶元素,使currentBlcok指向最新的栈顶元素。由于此时currentBlock不为null,所以执行currentBlock.push(vnode)【vue3源码】十三、认识Block
  13. 执行_createVNode(_component_ComA),创建vnode过程中,因为vnode.patchFlag === PatchFlag.PROPS,所以会将vnode添加到currentBlock中。【vue3源码】十三、认识Block
  14. 执行_createElementBlock('div')。先创建vnode,因为isBlockNodetrue,所以不会收集到currentBlock中。
  15. 执行setupBlock(),将currentBlock赋给vnode.dynamicChildren。然后执行closeBlock(),弹出栈顶元素,此时blockStack长度为0,所以currentBlock会指向null【vue3源码】十三、认识Block

最终生成的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中。