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

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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理