【vue3源码】十三、认识Block

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中。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容