美文网首页【vue3源码】
【vue3源码】十三、认识Block

【vue3源码】十三、认识Block

作者: MAXLZ | 来源:发表于2022-09-20 17:12 被阅读0次
    vue-13.png

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

    block-sfc-playground.png

    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状态如下如:

      block-example-01.png
    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中的最后一个元素。如下图所示:

      block-example-02.png
    8. 由于此时currentBlocknull,所以跳过currentBlock.push(vnode)

    9. item = 2、item = 3时,过程与4-7步骤相同。当item = 3时,block创建完毕后的状态如下:

      block-example-03.png
    10. 此时,list渲染完毕,接着调用_createElementBlock(_Fragment)

    11. 执行_createElementBlock的过程中,因为isBlockNode参数为truecurrentBlocknull,所以不会被currentBlock收集

    12. 执行setupBlock,将EMPTY_ARR(空数组)赋值给vnode.dynamicChildren,并调用closeBlock(),弹出栈顶元素,使currentBlcok指向最新的栈顶元素。由于此时currentBlock不为null,所以执行currentBlock.push(vnode)

      block-example-04.png
    13. 执行_createVNode(_component_ComA),创建vnode过程中,因为vnode.patchFlag === PatchFlag.PROPS,所以会将vnode添加到currentBlock中。

      block-example-05.png
    14. 执行_createElementBlock('div')。先创建vnode,因为isBlockNodetrue,所以不会收集到currentBlock中。

    15. 执行setupBlock(),将currentBlock赋给vnode.dynamicChildren。然后执行closeBlock(),弹出栈顶元素,此时blockStack长度为0,所以currentBlock会指向null

      block-example-06.png

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

    相关文章

      网友评论

        本文标题:【vue3源码】十三、认识Block

        本文链接:https://www.haomeiwen.com/subject/uodvortx.html