Vue3源码解读之编译优化

1,186 阅读17分钟

前言

Vue 3 是一个结合了编译时和运行时的框架。在编译时,我们编写的模板代码会被转换为一个 render 函数,该函数的返回值是一个虚拟节点。而在运行时,核心工作就是将虚拟节点转换为真实节点,然后根据情况对 DOM 树进行挂载或更新。前面的文章已经分析了虚拟节点转换为真实节点的核心流程,但是有些细节并没有讲解。这是因为这些内容和本文的主题 Block TreePatchFlags 相关,没有这些背景知识很难理解那些内容。

本文将从一段模板代码开始,并将模板代码和对应的编译结果进行比较。接着,引出了虚拟节点的 PatchFlags 属性值,并在 patchFlag 机制的基础上,讲解了 dynamicChildren 属性存在的意义。然后,分析为虚拟节点添加 dynamicChildren 属性值的过程,也就是 Block 机制。有了 Block 机制,我们又继续探讨 Block 机制的缺陷,进而分析 Block Tree

1. 模版编译

Diff 算法无法避免新旧虚拟 DOM 中无用的比较操作,通过 patchFlags标记动态内容,可以实现快速 diff 算法

<!-- 代码示例1 -->
<div>
  <h1>Hello Zhang</h1>
  <span>{{name}}</span>
</div>

template 经过模板编译会变成以下代码:

// 代码示例2
import {
  createElementVNode as _createElementVNode,
  toDisplayString as _toDisplayString,
  openBlock as _openBlock,
  createElementBlock as _createElementBlock,
} from "vue";

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createElementBlock("div", null, [
      _createElementVNode("h1", null, "Hello Zhang"),
      _createElementVNode(
        "span",
        null,
        _toDisplayString(_ctx.name),
        1 /* TEXT */
      ),
    ])
  );
}

// Check the console for the AST

Vue3 官方提供了模版编译成的 render 渲染函数:Vue 3 Template Explorer,大家也可以访问试试

vue3-template-explorer3.png

我们使用runtime-dom包来看下render函数执行后返回的虚拟节点是怎么样的

这里直接使用打包后的runtime-dom.esm-browser.js

大家可以直接下载下来,由于 type="module" 的限制,需要在本地启动一个服务器,然后在浏览器中访问该 HTML 页面。在控制台中,可以查看打印的调用该 render 函数生成的虚拟节点的结果。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>

  <body>
    <script type="module">
      import {
        toDisplayString as _toDisplayString,
        createElementVNode as _createElementVNode,
        Fragment as _Fragment,
        openBlock as _openBlock,
        createElementBlock as _createElementBlock,
        renderList as _renderList,
        createCommentVNode as _createCommentVNode,
        createTextVNode as _createTextVNode,
      } from "./runtime-dom.esm-browser.js";

      function render(_ctx, _cache, $props, $setup, $data, $options) {
        return (
          _openBlock(),
          _createElementBlock("div", null, [
            _createElementVNode("h1", null, "Hello Zhang"),
            _createElementVNode(
              "span",
              null,
              _toDisplayString(_ctx.name),
              1 /* TEXT */
            ),
          ])
        );
      }

      let vNode = render({ a: "a", b: "b" });
      console.log(vNode);
    </script>
  </body>
</html>

关于上面代码中使用到的 api,初次看到的人可能会感到疑惑,脑海中会出现一些问题,例如:_createElementVNode 是用来做什么的?_createElementBlock 又是用来做什么的?openBlock 又是用来做什么的?以及 1 /* TEXT */ 代表什么意思?

如果你对这些问题感到困惑,先不要着急,接下来的内容将会为你揭示这些地方背后的工作原理,让你豁然开朗。

2. render 函数

关于模板代码是如何被转换成渲染函数的,我们会在后续的文章中进行详细分析。现在,我们先来看看这个编译后的渲染函数究竟做了什么,或者说应该做什么。事实上,正如我们之前在文章中提到的那样,Vue 3 最核心的工作流程就是将模板代码转换为可以返回虚拟节点的渲染函数,以及将虚拟节点转换为真实节点。而渲染函数中自然就是返回一个虚拟节点对象。

我们可以看下渲染函数中调用的函数 _createElementVNode,发现这个函数其实就是用来创建虚拟节点的。但是你可能会感到困惑,因为创建虚拟节点的函数其实就是返回一个对象,这很好理解,这个对象可以描述一个 DOM 节点,而且也不难理解 DOM 节点有子节点,这里的虚拟节点也有子虚拟节点,因此函数 _createElementVNode 的第三个参数是一个数组,这个数组中的每一个元素都是调用函数 _createElementVNode 来创建的子虚拟节点

这些内容都很容易理解。但是有的同学会问了:在模板代码中,有一个根节点,但在渲染函数中,我们似乎只创建了子节点,那么根节点是由谁来创建的呢?

我们可以再看看上面的渲染函数,发现函数 _createElementBlock 的参数和函数 _createElementVNode 的参数几乎是一模一样的。没错,我们可以认为 _createElementBlock 的功能也是创建虚拟节点

这样,我们知道了渲染函数的核心任务就是返回虚拟节点,并且也知道了所谓的虚拟节点其实就是一个描述 DOM 节点的对象,而函数 _createElementVNode_createElementBlock 都具备创建该对象的能力。但是,由于这两个创建虚拟节点的函数名称都有差异,背后肯定也存在着深刻的原因。这正是我们接下来需要讨论的主题——PatchFlagsBlock Tree 的深刻联系。

3. PatchFlags 优化

let vNode = render({ a: "a", b: "b" });
console.log(vNode);

我们看看上面渲染函数生成的虚拟 DOM 结构是怎样的:

{
  "__v_isVNode": true,
  "__v_skip": true,
  "type": "div",
  "props": null,
  "key": null,
  "ref": null,
  "scopeId": null,
  "slotScopeIds": null,
  "children": [{}, {}],
  "dynamicChildren": [{}],
  "component": null,
  "suspense": null,
  "ssContent": null,
  "ssFallback": null,
  "dirs": null,
  "transition": null,
  "el": null,
  "anchor": null,
  "target": null,
  "targetAnchor": null,
  "staticCount": 0,
  "shapeFlag": 17,
  "patchFlag": 0,
  "dynamicProps": null,
  "appContext": null
}

我们可以发现虚拟 Node 有一个属性叫patchFlag。其实在代码中有个PatchFlags枚举如下:

3.1 动态标识

export const enum PatchFlags {
  TEXT = 1, // 动态文本节点
  CLASS = 1 << 1, // 动态class
  STYLE = 1 << 2, // 动态style
  PROPS = 1 << 3, // 除了class\style动态 props 的节点
  FULL_PROPS = 1 << 4, // 有key,需要完整diff
  HYDRATE_EVENTS = 1 << 5, // 挂载过事件的
  STABLE_FRAGMENT = 1 << 6, // 稳定序列,子节点顺序不会发生变化
  KEYED_FRAGMENT = 1 << 7, // 子节点有key的fragment
  UNKEYED_FRAGMENT = 1 << 8, // 子节点没有key的fragment
  NEED_PATCH = 1 << 9, // 进行非props比较, ref比较
  DYNAMIC_SLOTS = 1 << 10, // 动态插槽
  DEV_ROOT_FRAGMENT = 1 << 11,
  HOISTED = -1, // 表示静态节点,内容变化,不比较儿子
  BAIL = -2 // 表示diff算法应该结束
}

PatchFlags 源码

需要了解的是,除了 HOISTEDBAIL,其他所有的值都代表着虚拟节点所代表的节点是动态的。所谓动态的,就是可能发生变化的。比如 <div>abc</div> 这样的节点就不是动态的,里面没有响应式元素,正常情况下是不会发生变化的。在 patch 过程中对其进行比较是没有意义的。因此,Vue 3 对虚拟节点打上标记,如果节点的标记大于 0,则说明在 patch 的时候需要比较新旧虚拟节点的差异进行更新。

这时候有的同学会问了,如果只是区分节点是否是动态的,直接打上标记大于 0 或者小于 0 不就行了吗?为什么要使用十几个枚举值来表示呀?

这个问题问得很好。在回答这个问题之前,我们先问大家另外一个问题:假设让我们来比较两个节点有什么差异,你会怎么比较呢?

面对这个问题,按照正常的思维,既然要比较两个事物是否有差异,就得看两个事物的各组成部分是否有差异。我们知道虚拟节点有标签名、类型名、事件名等各种属性名,同时还有子节点,子节点又可能有子节点。那么要比较两个虚拟节点的差异,就得逐个属性逐级进行比较。而这样必然导致全部属性遍历,性能不可避免的会很低效。

Vue 3 的作者不仅标记某个虚拟节点是否动态,而且精准地标记具体是哪个属性是动态的。这样在进行更新的时候,只需要定向查找相应属性的状态。比如,patchFlag 的值如果包含的状态是 CLASS 对应的值 1<<1,则直接比对新旧虚拟节点的 class 属性的值的变化。注意,由于 patchFlag 是采用位运算的方式进行赋值,结合枚举类型 PatchFlagspatchFlag 可以同时表示多种状态。也就是说,可以表示 class 属性是动态的,也可以表示 style 属性是动态的。

我们发现,虽然已经精准地标记了虚拟节点的动态属性,甚至标识到了具体哪个属性的维度。但是仍然无法避免递归整个虚拟节点树。作为追求极致的工程师们,又创造性地想到了利用 Block 的机制来规避全量对虚拟节点树进行递归。

4. Block

在讲解 Block 机制之前,我们可以先尝试想一下,如果让我们自己来想办法规避全量比较虚拟节点,我们会怎么做呢?也许有的同学会想到,能不能把那些动态的节点单独放到一个地方进行维护呢?这样,新旧虚拟节点的动态节点就可以在同一个地方进行比较,我们再看看下面这个虚拟节点数据:

{
  "__v_isVNode": true,
  "__v_skip": true,
  "type": "div",
  "props": null,
  "key": null,
  "ref": null,
  "scopeId": null,
  "slotScopeIds": null,
  "children": [{}, {}],
  "dynamicChildren": [{}],
  "component": null,
  "suspense": null,
  "ssContent": null,
  "ssFallback": null,
  "dirs": null,
  "transition": null,
  "el": null,
  "anchor": null,
  "target": null,
  "targetAnchor": null,
  "staticCount": 0,
  "shapeFlag": 17,
  "patchFlag": 0,
  "dynamicProps": null,
  "appContext": null
}

可以看到有一个 dynamicChildren 属性。一般的虚拟节点是没有这个属性的,因为我们之前说过,虚拟节点是用来描述 DOM 节点的对象,而 DOM 节点是没有一个叫做 dynamicChildren 的属性的。那么这个属性有什么用呢?还记得我们在分析 patchElement 函数的时候,有这样一段代码

// 代码示例5
if (dynamicChildren) {
  patchBlockChildren(
    n1.dynamicChildren!,
    dynamicChildren,
    el,
    parentComponent,
    parentSuspense,
    areChildrenSVG,
    slotScopeIds
  )
  if (__DEV__) {
    // necessary for HMR
    traverseStaticChildren(n1, n2)
  }
} else if (!optimized) {
  // full diff
  patchChildren(
    n1,
    n2,
    el,
    null,
    parentComponent,
    parentSuspense,
    areChildrenSVG,
    slotScopeIds,
    false
  )
}

我们来看看函数patchBlockChildren的具体实现:

// 代码示例6
// 用于处理动态节点的更新
const patchBlockChildren: PatchBlockChildrenFn = (
  oldChildren, // 旧的虚拟节点数组
  newChildren, // 新的虚拟节点数组
  fallbackContainer, // 回退容器
  parentComponent, // 父组件
  parentSuspense, // 父 suspense
  isSVG, // 是否为 SVG 元素
  slotScopeIds // 插槽作用域 ID
) => {
  // 遍历新的虚拟节点数组
  for (let i = 0; i < newChildren.length; i++) {
    const oldVNode = oldChildren[i] // 获取旧的虚拟节点
    const newVNode = newChildren[i] // 获取新的虚拟节点
    // 确定要进行更新的容器(父元素)
    const container =
      // oldVNode 可能是一个错误的异步 setup() 组件,位于 suspense 内部,
      // 它将不会有一个已挂载的元素
      oldVNode.el &&
      // - 对于 Fragment,我们需要提供 Fragment 本身的实际父级
      // 以便它可以移动其子级。
      (oldVNode.type === Fragment ||
        // - 对于不同的节点,将会有一个替换,
        // 这也需要正确的父容器
        !isSameVNodeType(oldVNode, newVNode) ||
        // - 对于组件,它可以包含任何内容。
        oldVNode.shapeFlag & (ShapeFlags.COMPONENT | ShapeFlags.TELEPORT))
        ? hostParentNode(oldVNode.el)!
        : // 在其他情况下,父容器实际上并没有被使用,因此我们
          // 只需在此处传递块元素以避免 DOM parentNode 调用。
          fallbackContainer
    // 调用 patch 函数进行更新操作
    patch(
      oldVNode,
      newVNode,
      container,
      null,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      true
    )
  }
}

patchElement 函数的逻辑还是挺简单的,先对新旧虚拟节点的 dynamicChildren 属性所代表的虚拟节点数组进行遍历,然后调用 patch 函数进行更新操作。

由此可以看出性能被大幅度提升,从 tree 级别的比对,变成了线性结构比对。

从【代码示例 5】 中可以发现,如果属性 dynamicChildren 有值,则不会执行 patchChildren 函数进行比较新旧虚拟节点的差异并进行更新。有的同学问了,为什么可以直接比较虚拟节点的 dynamicChildren 属性对应的数组元素,就可以完成更新呢?

我们知道,dynamicChildren 中存放的是所有的代表动态节点的虚拟节点,而且从【代码示例 1】中可以看出,dynamicChildren 记录的动态节点不仅包括自己所属层级的动态节点,也包括子级的动态节点,也就是说根节点内部所有的动态节点都会收集在 dynamicChildren 中。由于新旧虚拟节点的根节点下都有 dynamicChildren 属性,都保存了所有的动态元素对应的值,也就是说动态节点的顺序是一一对应的。因此,在【代码示例 6】中,不再需要深度递归去寻找节点间的差异,而是简单的线性遍历并执行 patch 函数就可以完成节点的更新。

那么,这个 dynamicChildren 属性是如何赋值的呢?

还记得【代码示例 2】中,让我们倍感疑惑的两个函数_openBlock 和_createElementBlock 吗。我们来探索这两个函数的内部实现:

openBlock 源码

export function openBlock(disableTracking = false) {
  blockStack.push((currentBlock = disableTracking ? null : []));
}

我们可以看到 openBlock 函数的逻辑非常简单,它只是给数组 blockStack 添加一个元素,该元素可能为 null 或者空数组 []

再来看看 createElementBlock 是做什么的

createElementBlock

// 定义 setupBlock 函数,用于设置 Block 相关的信息
function setupBlock(vnode: VNode) {
  // 在 Block 虚拟节点上保存当前 Block 的子节点
  vnode.dynamicChildren =
    isBlockTreeEnabled > 0 ? currentBlock || (EMPTY_ARR as any) : null
  // 关闭当前 Block
  closeBlock()
  // 由于 Block 一定会被更新,因此将其作为其父 Block 的子节点进行跟踪
  if (isBlockTreeEnabled > 0 && currentBlock) {
    currentBlock.push(vnode)
  }
  return vnode
}

/**
 * @private
 */
// 定义 createElementBlock 函数,用于创建 Block 类型的虚拟节点
export function createElementBlock(
  type: string | typeof Fragment, // 节点类型
  props?: Record<string, any> | null, // 节点属性
  children?: any, // 子节点
  patchFlag?: number, // 补丁标志
  dynamicProps?: string[], // 动态属性
  shapeFlag?: number // 节点类型标志
) {
  return setupBlock(
    createBaseVNode(
      type,
      props,
      children,
      patchFlag,
      dynamicProps,
      shapeFlag,
      true /* isBlock */ // 标记为 Block 类型
    )
  )
}

调用了一个函数 createBaseVNode,该函数用于创建虚拟节点对,那这里的函数 setupBlock 发挥了什么作用呢?而 可以概括为以下三个作用:

  • 在虚拟节点创建完成后,给该虚拟节点的 dynamicChildren 属性赋值,赋的值为 currentBlock。我们知道,currentBlock 是在调用 openBlock 函数的时候初始化的一个数组。

  • 调用 closeBlock 函数的作用是将调用 openBlock 时初始化的数组对象 currentBlock 移除,并将 currentBlock 赋值为 blockStack 的最后一个元素。

// 代码示例9
export function closeBlock() {
  blockStack.pop();
  // 关闭block
  currentBlock = blockStack[blockStack.length - 1] || null;
}
  • 执行语句 currentBlock.push(vnode),将当前创建的节点自身添加到上一级(因为 closeBlock 的时候已经 pop 出刚刚创建完成的虚拟节点所在的 currentBlock)currentBlock 中。

描述了上面 3 点,可能大家觉得有些疑惑,上面的描述和代码虽然很一致,但是究竟发挥了什么作用呢?我们先将源码实现进行精简,在下文讨论Block Tree的时候再回过头看代码示例 7 到代码示例 9 的代码:

// 代码示例10
export function createElementBlock(
  type: string | typeof Fragment,
  props?: Record<string, any> | null,
  children?: any,
  patchFlag?: number,
  dynamicProps?: string[],
  shapeFlag?: number
) {
  return setupBlock(
    createBaseVNode(/*此处省略若干参数*/)
  )
}

function createBaseVNode(/* ...*/) {
  const vnode = { /* ...*/} as VNode
  if (/*如果是动态元素*/) {
    currentBlock.push(vnode)
  }
  return vnode
}

function setupBlock(vnode: VNode) {
  vnode.dynamicChildren = currentBlock
  return vnode
}

4.1 Block 存在的问题

上面已经了解了,dynamicChildren 的赋值过程能够提高我们更新 DOM 元素的效率。但是,这里面还是存在一些棘手的问题。关键问题在于,当 DOM 结构不稳定时,我们无法像【代码示例 6】那样更新元素。

因为如果我们想通过遍历数组的方式调用 patch 函数来更新元素,那么新旧虚拟 Node 的 dynamicChildren 元素必须是一一对应的。这是因为只有当新旧虚拟 Node 是同一个元素时,依次调用 patch 进行更新才有意义。但如果新旧虚拟 Node 的dynamicChildren元素不能一一对应,那我们就无法用这种方式来更新。

然而,我们平常写的的模版代码中,有很多可能会改变 DOM 树结构的指令,比如v-if、v-else、v-else-if、v-for等(具体的指令下面介绍)。例如,下面的模板:

<!--代码示例11-->
<div>
  <div v-if="flag">
    <div>{{name}}</div>
    <div>{{age}}</div>
  </div>
  <div v-else>
    <div>{{city}}</div>
  </div>
  <div v-for="item in arr">{{item}}</div>
</div>

当 flag 的值改变时,收集的动态节点数量会有所不同,同时,不同的虚拟 Node 对应的真实 DOM 也会有所不同。因此,如果我们试图像在代码片段 6 中那样直接遍历并更新,这种方法就无法生效。

让我们来举个例子。当 flag 为 true 时,动态节点中包含{{name}}{{age}}所在的 div。但是,当条件发生变化后,新的虚拟 Node 收集的动态节点变成了{{city}}所在的 div。在进行遍历比较时,会用{{city}}所在 div 的虚拟 Node 去和{{name}}所在 div 的虚拟 Node 进行比较和更新。但问题在于,{{name}}所在 div 的虚拟 Node 的 el 属性是节点<div>{{name}}</div>,而这个节点因为条件的变化已经消失了。所以,即使我们对这个节点进行了更新,浏览器页面也不会有任何变化。

4.2 Block Tree

为什么我们还要提出 blockTree 的概念? 只有 block 不就挺好的么? 问题出在 block 在收集动态节点时是忽略虚拟 DOM 树层级的。

为了解决仅使用 Block 来提升更新性能时出现的问题,就有了Block Tree。所谓的Block Tree,其实就是将那些可能发生 DOM 结构变化的地方也作为一个动态节点进行收集。实际上,【代码示例 6】到【代码示例 9】之间维护的全局栈结构,就是为了配合Block Tree这种机制的正常运作。

我们来看一个具体的例子:

<!--代码示例12-->
<div>
  <div>{{name}}</div>
  <div v-for="(item,index) in arr" :key="index">{{item}}</div>
</div>

我们来看一下这个 render 函数的返回值。为了方便阅读,我做了大量精简,关键信息如下

{
    "type": "div",
    "children": [
        {
            "type": "div",
            "key": null,
            "children": "james",
            "staticCount": 0,
            "shapeFlag": 9,
            "patchFlag": 1,
            "dynamicProps": null,
            "dynamicChildren": null,
        },
        {
            "key": null,
            "slotScopeIds": null,
            "children": [
                {
                    "type": "div",
                    "key": 0,
                    "children": "aaa",
                    "props": {
                      "key": 0
                    },
                    "patchFlag": 1,
                },
                {
                    "type": "div",
                    "key": 0,
                    "children": "bbb",
                    "props": {
                      "key": 0
                    },
                    "patchFlag": 1,
                },
                {
                    "type": "div",
                    "key": 2,
                    "children": "ccc",
                    "props": {
                      "key": 2
                    },
                    "patchFlag": 1,
                }
            ],
        }
    ],
    "patchFlag": 0,
    "dynamicChildren": [
        {
            "type": "div",
            "children": "james",
            "shapeFlag": 9,
            "patchFlag": 1,
            "dynamicProps": null,
            "dynamicChildren": null,
        },
        {
            "props": null,
            "key": null,
            "children": [
                {
                    "type": "div",
                    "key": 0,
                    "children": "aaa",
                    "props": {
                      "key": 0
                    },
                    "patchFlag": 1,
                },
                {
                    "type": "div",
                    "key": 1,
                    "children": "bbb",
                    "props": {
                      "key": 1
                    },
                    "patchFlag": 1,
                },
                {
                    "type": "div",
                    "key": 2,
                    "children": "ccc",
                    "props": {
                      "key": 2
                    },
                    "patchFlag": 1,
                }
            ],
        }
    ],
    "appContext": null
}

我们可以看到,根节点下有一个dynamicChildren属性值,这个属性对应的数组有两个元素。一个元素对应{{name}}所在的 div,另一个元素对应 for 循环的外层节点。这个外层节点的dynamicChildren是一个空数组,原因是我们无法保证里面的元素数量的一致性,也就无法通过循环遍历,让新旧虚拟节点一一对应进行更新。因此,我们只能正常比较 children 下的元素。

我们再来看看这种场景的模版代码:

<div>
  <p v-if="flag">
    <span>{{a}}</span>
  </p>
  <div v-else>
    <span>{{a}}</span>
  </div>
</div>

这里我们知道默认根节点是一个 block 节点,如果要是按照之前的套路来搞,这时候切换 flag 的状态将无法从 p 标签切换到 div 标签。 解决方案:就是将不稳定的结构也作为 block 来进行处理

4.3 不稳定结构

所谓的不稳定结构就是 DOM 树的结构可能会发生变化。不稳定结构有哪些呢? (v-if/v-for/Fragment

4.3.1 v-if

<div>
  <div v-if="flag">
    <span>{{a}}</span>
  </div>
  <div v-else>
    <p><span>{{a}}</span></p>
  </div>
</div>

使用template-explore查看编译后的结果:

import {
  toDisplayString as _toDisplayString,
  createElementVNode as _createElementVNode,
  openBlock as _openBlock,
  createElementBlock as _createElementBlock,
  createCommentVNode as _createCommentVNode,
} from "vue";

const _hoisted_1 = { key: 0 };
const _hoisted_2 = { key: 1 };

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createElementBlock("div", null, [
      _ctx.flag
        ? (_openBlock(),
          _createElementBlock("div", _hoisted_1, [
            _createElementVNode(
              "span",
              null,
              _toDisplayString(_ctx.a),
              1 /* TEXT */
            ),
          ]))
        : (_openBlock(),
          _createElementBlock("div", _hoisted_2, [
            _createElementVNode("p", null, [
              _createElementVNode(
                "span",
                null,
                _toDisplayString(_ctx.a),
                1 /* TEXT */
              ),
            ]),
          ])),
    ])
  );
}

// Check the console for the AST
Block(div)
	Blcok(div,{key:0})
	Block(div,{key:1})

父节点除了会收集动态节点之外,也会收集子 block。 更新时因 key 值不同会进行删除重新创建

4.3.2 v-for

随着v-for变量的变化也会导致虚拟 DOM 树变得不稳定

看看这个简单的循环生成 dom 节点:

<div>
  <div v-for="item in fruits"></div>
</div>

模板编译成[渲染函数]:

template-explorer

import {
  renderList as _renderList,
  Fragment as _Fragment,
  openBlock as _openBlock,
  createElementBlock as _createElementBlock,
} from "vue";

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createElementBlock("div", null, [
      (_openBlock(true),
      _createElementBlock(
        _Fragment,
        null,
        _renderList(_ctx.fruits, (item) => {
          return _openBlock(), _createElementBlock("div");
        }),
        256 /* UNKEYED_FRAGMENT */
      )),
    ])
  );
}

// Check the console for the AST

可以试想一下,如果不增加这个 block(_openBlock),前后元素不一致是无法做到靶向更新的。因为 dynamicChildren 中还有可能有其他层级的元素。同时这里还生成了一个 Fragment,因为前后元素个数不一致,所以称之为不稳定序列