关于前端:vue3源码十三认识Block

34次阅读

共计 8329 个字符,预计需要花费 21 分钟才能阅读完成。

什么是 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)[] = []
// 一个数组,用于存储动静节点,最终会赋给 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 状态如下如:
  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 中。

正文完
 0