coverPiccoverPic

Vue 3 渲染优化:Block

前言

Vue 3 中,使用 Block 对渲染进行了优化,某些 VNode 中有dynamicChildren属性,它收集了动态子代节点的 VNode,当存在dynamicChildren时,diff 的时候只需要比较dynamicChildren中的 VNode, 相比前代的全量比较,提高了性能。

render 函数

模板编译部分就不看了,分支太多了,总之,Vue 会为组件根节点(多节点则自动创建template作为根节点)、v-ifv-for、SVG 节点等情况创建 Block。举个栗子🌰:

html
  1. <div>
  2. <div>TEST</div>
  3. <div>{{data}}</div>
  4. <div :data="test">TEMP</div>
  5. <div v-for="item in arr">{{item}}</div>
  6. <div v-if="data">IF</div>
  7. <div v-else-if="count">ELIF</div>
  8. <div v-else>ELSE</div>
  9. </div>

编译后:

ts
  1. const _Vue = Vue
  2. const { createElementVNode: _createElementVNode, createCommentVNode: _createCommentVNode } = _Vue
  3. const _hoisted_1 = /*#__PURE__*/_createElementVNode("div", null, "TEST", -1 /* HOISTED */)
  4. const _hoisted_2 = ["data"]
  5. const _hoisted_3 = { key: 0 }
  6. const _hoisted_4 = { key: 1 }
  7. const _hoisted_5 = { key: 2 }
  8. return function render(_ctx, _cache) {
  9. with (_ctx) {
  10. const { createElementVNode: _createElementVNode, toDisplayString: _toDisplayString, renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode } = _Vue
  11. return (_openBlock(), _createElementBlock("div", null, [
  12. _hoisted_1,
  13. _createElementVNode("div", null, _toDisplayString(data), 1 /* TEXT */),
  14. _createElementVNode("div", { data: test }, "TEMP", 8 /* PROPS */, _hoisted_2),
  15. (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(arr, (item) => {
  16. return (_openBlock(), _createElementBlock("div", null, _toDisplayString(item), 1 /* TEXT */))
  17. }), 256 /* UNKEYED_FRAGMENT */)),
  18. data
  19. ? (_openBlock(), _createElementBlock("div", _hoisted_3, "IF"))
  20. : count
  21. ? (_openBlock(), _createElementBlock("div", _hoisted_4, "ELIF"))
  22. : (_openBlock(), _createElementBlock("div", _hoisted_5, "ELSE"))
  23. ]))
  24. }
  25. }

我们可以看到根节点,v-if系列、v-for都使用_openBlock来创建 Block,_createElementBlock进行后续的处理。

创建 Block

来看openBlock的代码:

ts
  1. export const blockStack: (VNode[] | null)[] = []
  2. export let currentBlock: VNode[] | null = null
  3. export function openBlock(disableTracking = false) {
  4. blockStack.push((currentBlock = disableTracking ? null : []))
  5. }

Block 是一个栈结构,用于处理 Block 嵌套的问题,而且,Block 不能收集子 Block 的子节点。根据dynamicChildren diff 需要前后子代节点顺序一致。遇到可能不一致的地方,Vue 可以再加入一个 Block,把 Block 的根节点插入上级 Block 中,对于子 Block 中不稳定的子节点结构,不收集dynamicChildren,diff 时降级依旧使用全量对比。这样子可以避免比对时子代节点顺序不一致的问题。

收集动态子代节点

再来看 VNode,根节点的dynamicChildren有 4 个元素,分别是<div>{{data}}</div><div :data="test">TEMP</div>两个动态节点,以及v-for的根节点和v-if 3 个分支中,其中一个的根节点。静态节点是不在里面的。

在编译后的 render 函数里面:

ts
  1. (_openBlock(), _createElementBlock("div", null, [
  2. _hoisted_1,
  3. _createElementVNode("div", null, _toDisplayString(data), 1 /* TEXT */),
  4. _createElementVNode("div", { data: test }, "TEMP", 8 /* PROPS */, _hoisted_2),
  5. (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(arr, (item) => {
  6. return (_openBlock(), _createElementBlock("div", null, _toDisplayString(item), 1 /* TEXT */))
  7. }), 256 /* UNKEYED_FRAGMENT */)),
  8. data
  9. ? (_openBlock(), _createElementBlock("div", _hoisted_3, "IF"))
  10. : count
  11. ? (_openBlock(), _createElementBlock("div", _hoisted_4, "ELIF"))
  12. : (_openBlock(), _createElementBlock("div", _hoisted_5, "ELSE"))
  13. ]))

可以看见,创建根节点时,_createElementBlock的第 3 个入参,是创建好了的子节点数组,很显然,创建子节点是依据 DFS 从下到上,从前到后的顺序进行的。_createElementBlock中有关闭 Block 的逻辑,这样子就确保了,关闭 Block 的时候 Block 已经收集完子代节点了。

createBaseVNode用于创建标签、组件、文本等类型的 VNode。节点是否加入 Block 的相关逻辑如下:

ts
  1. function createBaseVNode(
  2. type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  3. props: (Data & VNodeProps) | null = null,
  4. children: unknown = null,
  5. patchFlag = 0,
  6. dynamicProps: string[] | null = null,
  7. shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,
  8. isBlockNode = false,
  9. needFullChildrenNormalization = false,
  10. ) {
  11. // ...
  12. // track vnode for block tree
  13. if (
  14. isBlockTreeEnabled > 0 &&
  15. // avoid a block node from tracking itself
  16. !isBlockNode &&
  17. // has current parent block
  18. currentBlock &&
  19. // presence of a patch flag indicates this node needs patching on updates.
  20. // component nodes also should always be patched, because even if the
  21. // component doesn't need to update, it needs to persist the instance on to
  22. // the next vnode so that it can be properly unmounted later.
  23. (vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
  24. // the EVENTS flag is only for hydration and if it is the only flag, the
  25. // vnode should not be considered dynamic due to handler caching.
  26. vnode.patchFlag !== PatchFlags.NEED_HYDRATION
  27. ) {
  28. currentBlock.push(vnode)
  29. }
  30. // ...
  31. return vnode
  32. }

在这里可以看到,vnode.patchFlag > 0才会被收集,说明动态的 VNode 才会被收集。patchFlag不大于 0 有两种情况:-1:被提升的静态节点;-2:一些特殊节点,例如手写 JSX,它不会使用 Block 的优化逻辑。

而且,这里的currentBlock会收集子代的节点,而不是只有子节点。试想一下,组件根节点创建了 Block,而其孙节点是动态的,之间没有创建 Block 的子节点,这样子,孙节点也会被组件根节点的currentBlock收集。

还有一些特殊情况:

ts
  1. function _createVNode(
  2. type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  3. props: (Data & VNodeProps) | null = null,
  4. children: unknown = null,
  5. patchFlag: number = 0,
  6. dynamicProps: string[] | null = null,
  7. isBlockNode = false,
  8. ): VNode {
  9. // ...
  10. if (isVNode(type)) {
  11. // createVNode receiving an existing vnode. This happens in cases like
  12. // <component :is="vnode"/>
  13. const cloned = cloneVNode(type, props, true /* mergeRef: true */)
  14. if (children) {
  15. normalizeChildren(cloned, children)
  16. }
  17. if (isBlockTreeEnabled > 0 && !isBlockNode && currentBlock) {
  18. if (cloned.shapeFlag & ShapeFlags.COMPONENT) {
  19. // 没太看懂为什么要怎么做
  20. currentBlock[currentBlock.indexOf(type)] = cloned
  21. } else {
  22. currentBlock.push(cloned)
  23. }
  24. }
  25. cloned.patchFlag |= PatchFlags.BAIL
  26. return cloned
  27. }
  28. // ...
  29. return createBaseVNode(
  30. type,
  31. props,
  32. children,
  33. patchFlag,
  34. dynamicProps,
  35. shapeFlag,
  36. isBlockNode,
  37. true,
  38. )
  39. }

这里是给component标签传入一个 VNode 实例的情况,感觉情况很少见。

关闭 Block

来看_createElementBlock,也就是createElementBlock

ts
  1. export function createElementBlock(
  2. type: string | typeof Fragment,
  3. props?: Record<string, any> | null,
  4. children?: any,
  5. patchFlag?: number,
  6. dynamicProps?: string[],
  7. shapeFlag?: number,
  8. ) {
  9. return setupBlock(
  10. createBaseVNode(
  11. type,
  12. props,
  13. children,
  14. patchFlag,
  15. dynamicProps,
  16. shapeFlag,
  17. true /* isBlock */,
  18. ),
  19. )
  20. }

createBaseVNode传入创建好的 Block 根节点 VNode。类似地,还有函数createBlock,这个常用于templatecomponent这样子的特殊标签。

ts
  1. export function createBlock(
  2. type: VNodeTypes | ClassComponent,
  3. props?: Record<string, any> | null,
  4. children?: any,
  5. patchFlag?: number,
  6. dynamicProps?: string[],
  7. ): VNode {
  8. return setupBlock(
  9. createVNode(
  10. type,
  11. props,
  12. children,
  13. patchFlag,
  14. dynamicProps,
  15. true /* isBlock: prevent a block from tracking itself */,
  16. ),
  17. )
  18. }

setupBlock用于使当前 Block 出栈,并且记录dynamicChildren,并且把当前 Block 根节点记录到上一级的 Block 中:

ts
  1. export function closeBlock() {
  2. blockStack.pop()
  3. currentBlock = blockStack[blockStack.length - 1] || null
  4. }
  5. function setupBlock(vnode: VNode) {
  6. // save current block children on the block vnode
  7. vnode.dynamicChildren =
  8. isBlockTreeEnabled > 0 ? currentBlock || (EMPTY_ARR as any) : null
  9. // close block
  10. closeBlock()
  11. // a block is always going to be patched, so track it as a child of its
  12. // parent block
  13. if (isBlockTreeEnabled > 0 && currentBlock) {
  14. currentBlock.push(vnode)
  15. }
  16. return vnode
  17. }

结语

这里描述了 Vue 3 渲染优化中的 Block 创建流程。Block 通过 DFS 的顺序,收集同一个 Block 下的子代动态节点,达到 diff 的时候减少比对的目的。对于可能不稳定的结构,通过新加入新 Block 来使上层 Block 的 VNode 有一个稳定的顺序。

0 条评论未登录用户
Ctrl or + Enter 评论
🌸 Run