Skip to content

模板编译提升

面试题:说一下 Vue3 在进行模板编译时做了哪些优化?

  1. 静态提升
  2. 预字符串化
  3. 缓存事件处理函数
  4. Block Tree
  5. PatchFlag

静态提升

静态提升 Static Hoisting,在模板编译阶段识别并提升不变的静态节点到渲染函数外部,从而减少每次渲染时的计算量。被提升的节点无需重复创建。

哪些节点会被提升

  1. 元素节点
  2. 没有绑定动态内容的节点

一个提升的示例

vue
<template>
  <div>
    <p>这是一个静态的段落。</p>
    <p>{{ dynamicMessage }}</p>
  </div>
</template>

在 Vue2 时期不管是静态节点还是动态节点,都会编译为 创建虚拟节点函数 的调用。

js
with(this) {
  return createElement('div', [
    createElement('p', [createTextVNode("这是一个静态的段落。")]),
    createElement('p', [createTextVNode(toString(dynamicMessage))])
  ])
}

Vue3 中,编译器会对静态内容的编译结果进行提升

js
const _hoisted_1 = /*#__PURE__*/createStaticVNode("<p>这是一个静态的段落。</p>", 1);

export function render(_ctx, _cache) {
  return (openBlock(), createElementBlock("div", null, [
    _hoisted_1,
    createElementVNode("p", null, toDisplayString(_ctx.dynamicMessage), 1 /* TEXT */)
  ]))
}

除了静态节点,静态属性也是能够提升的,例如:

vue
<template>
  <button class="btn btn-primary">{{ buttonText }}</button>
</template>

在这个模板中,虽然 button 是一个动态节点,但是属性是固定的,因此这里也有优化的空间:

js
// 静态属性提升
const _hoisted_1 = { class: "btn btn-primary" };

export function render(_ctx, _cache) {
  return (openBlock(), createElementBlock("button", _hoisted_1, toDisplayString(_ctx.buttonText), 1 /* TEXT */))
}

预字符串化

当编译器遇到大量连续的静态内容时,会直接将其编译为一个普通的字符串节点。例如:

vue
<template>
  <div class="menu-bar-container">
    <div class="logo">
      <h1>logo</h1>
    </div>
    <ul class="nav">
      <li><a href="">menu</a></li>
      <li><a href="">menu</a></li>
      <li><a href="">menu</a></li>
      <li><a href="">menu</a></li>
      <li><a href="">menu</a></li>
    </ul>
    <div class="user">
      <span>{{ user.name }}</span>
    </div>
  </div>
</template>

<script setup>
import { ref } from "vue";
const user = ref({
  name: "zhangsan",
});
</script>

编译结果中和静态提升相关的部分:

js
const _hoisted_1 = { class: "menu-bar-container" }
const _hoisted_2 = /*#__PURE__*/_createStaticVNode("<div class=\"logo\"><h1>logo</h1></div><ul class=\"nav\"><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li><li><a href=\"\">menu</a></li></ul>", 2)
const _hoisted_4 = { class: "user" }

其中的 _hoisted_2 就是将连续的静态节点编译为了字符串。

思考🤔:这样有什么好处呢?

答案:当大量的连续的静态节点被编译为字符串节点后,整体的虚拟 DOM 节点数量就少了,自然而然 diff 的速度就更快了。

Vue2:

vue2

Vue3:

vue3

第二个好处就是在 SSR 的时候,无需重复计算和转换,减少了服务器端的计算量和处理时间。

思考🤔:大量连续的静态内容时,会启用预字符串化处理,大量连续的边界在哪里?

答案:在 Vue3 编译器内部有一个阀值,目前是 10 个节点左右会启动预字符串化。

vue
<template>
  <div class="menu-bar-container">
    <div class="logo">
      <h1>logo</h1>
      <h1>logo</h1>
      <h1>logo</h1>
      <h1>logo</h1>
      <h1>logo</h1>
      <h1>logo</h1>
      <h1>logo</h1>
      <h1>logo</h1>
      <h1>logo</h1>
    </div>
    <div class="user">
      <span>{{ user.name }}</span>
    </div>
  </div>
</template>

<script setup>
import { ref } from "vue";
const user = ref({
  name: "zhangsan",
});
</script>

缓存内联事件处理函数

模板在进行编译的时候,会针对内联的事件处理函数做缓存。例如:

vue
<button @click="count++">plus</button>

在 Vue2 中,每次渲染都会为这个内联事件创建一个新的函数,这会产生不必要的内存开销和性能损耗。

js
render(ctx){
  return createVNode("button", {
    // 每次渲染的时候,都会创建一个新的函数
    onClick: function($event){
      ctx.count++;
    }
  })
}

在 Vue3 中,为了优化这种情况,编译器会自动为内联事件处理函数生成缓存代码。

js
render(ctx, _cache){
  return createVNode("button", {
    // 如果缓存里面有,直接从缓存里面取
    // 如果缓存里面没有,创建一个新的事件处理函数,然后将其放入到缓存里面
    onClick: cache[0] || (cache[0] = ($event) => (ctx.count++))
  })
}

思考🤔:为什么仅针对内联事件处理函数进行缓存?

答案:非内联事件处理函数不需要缓存,因为非内联事件处理函数在组件实例化的时候就存在了,不会在每次渲染时重新创建。缓存机制主要是为了解决内联事件处理函数在每次渲染的时候重复创建的问题。

block tree

Vue2 在对比新旧树的时候,并不知道哪些节点是静态的,哪些是动态的,因此只能一层一层比较,这就浪费了大部分时间在比对静态节点上,例如下面的代码:

vue
<form>
  <div>
    <label>账号:</label>
    <input v-model="user.loginId" />
  </div>
  <div>
    <label>密码:</label>
    <input v-model="user.loginPwd" />
  </div>
</form>

20200929172002

每次状态更新时,Vue2 需要遍历整个虚拟 DOM 树来寻找差异。这种方法虽然通用,但在大型组件或复杂页面中,性能损耗会比较明显,因为它浪费了大量时间在静态节点的比较上。

思考🤔:前面不是说静态节点会提升么?

答案:静态提升解决的是不再重复生成静态节点所对应的虚拟DOM节点。现在要解决的问题是虚拟DOM树中静态节点比较能否跳过的问题。

什么是Block

一个 Block 本质上也是一个虚拟 DOM 节点,不过该虚拟 DOM 节点上面会多出来一个 dynamicChildren 属性,该属性对应的值为数组,数组里面存储的是动态子节点。以上面的代码为例,form 对应的虚拟 DOM 节点就会存在 dynamicChildren 属性:

20200929172555

有了 block 之后,就不需要再像 Vue2 那样一层一层,每个节点进行对比了,对比的粒度变成了直接找 dynamicChildren 数组,然后对比该数组里面的动态节点,这样就很好的实现了跳过静态节点比较。

哪些节点会成为 block 节点?

  1. 模板中的根节点都会是一个 block 节点。

    vue
    <template>
    	<!-- 这是一个block节点 -->
    	<div>
        <p>{{ bar }}</p>
      </div>
    	<!-- 这是一个block节点 -->
    	<h1>
        <span :id="test"></span>
      </h1>
    </template>
  2. 任何带有 v-if、v-else-if、v-else、v-for 指令的节点,也需要作为 block 节点。

    答案:因为这些指令会让虚拟DOM树的结构不稳定。

    vue
    <div>
      <section v-if="foo">
      	<p>{{ a }}</p>
      </section>
      <div v-else>
        <p>{{ a }}</p>
      </div>
    </div>

    按照之前的设计,div是一个 block 节点,收集到的动态节点只有 p. 这意味着无论 foo 是 true 还是 false,最终更新只会去看 p 是否发生变化,从而产生 bug.

    解决方案也很简单,让带有这些指令的节点成为一个 block 节点即可

    block(div)
    	- block(section)
    	- block(div)

    此时这种设计,父级block除了收集动态子节点以外,还会收集子block节点。

    多个 block 节点自然就形成了树的结构,这就是 block tree.

补丁标记

补丁标记 PatchFlags,这是 Vue 在做节点对比时的近一步优化。

即便是动态的节点,一般也不会是节点所有信息(类型、属性、文本内容)都发生了更改,而仅仅只是一部分信息发生更改。

之前在 Vue2 时期对比每一个节点时,并不知道这个节点哪些相关信息会发生变化,因此只能将所有信息依次比对,例如:

vue
<div :class="user" data-id="1" title="user name">
  {{user.name}}
</div>

在 Vue2 中:

  • 全面对比:会逐个去检查节点的每个属性(class、data-id、title)以及子节点的内容
  • 性能瓶颈:这种方式自然就存在一定的性能优化空间
20200929172805

在 Vue3 中,PatchFlag 通过为每个节点生成标记,显著优化了对比过程。编译器在编译模板时,能够识别哪些属性或内容是动态的,并为这些动态部分生成特定的标记。

Vue3 的 PatchFlag 包括多种类型,每种类型标记不同的更新需求:

  • TEXT:表示节点的文本内容可能会发生变化。
  • CLASS:表示节点的 class 属性是动态的,可能会发生变化。
  • STYLE:表示节点的 style 属性是动态的,可能会发生变化。
  • PROPS:表示节点的一个或多个属性是动态的,可能会发生变化。
  • FULL_PROPS:表示节点有多个动态属性,且这些属性不是简单的静态值。
  • HYDRATE_EVENTS:表示节点的事件监听器是动态的,需要在客户端进行水合处理。
  • STABLE_FRAGMENT:表示节点的子节点顺序稳定,允许按顺序进行更新。
  • KEYED_FRAGMENT:表示节点的子节点带有 key,可以通过 key 进行高效的更新。
  • UNKEYED_FRAGMENT:表示节点的子节点无 key,但可以通过简单的比较进行更新。

例如上面的代码,编译出来的函数:

js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", {
    class: _normalizeClass($setup.user),
    "data-id": "1",
    title: "user name"
  }, _toDisplayString($setup.user.name), 3 /* TEXT, CLASS */))
}

通过这些标记,Vue3 在更新时不再需要对每个属性都进行全面的对比,而是只检查和更新那些被标记为动态的部分,从而显著减少了不必要的计算开销。

面试题:说一下 Vue3 在进行模板编译时做了哪些优化?

参考答案:

Vue3 的编译器在进行模板编译的时候,主要做了这么一些优化:

  1. 静态提升:解决的是静态内容不要重复生成新的虚拟 DOM 节点的问题
  2. 预字符串化:解决的是大量的静态内容,干脆虚拟 DOM 节点都不要了,直接生成字符串,虚拟 DOM 节点少了,diff 的时间花费也就更少。
  3. 缓存内联事件处理函数:每次运行渲染函数时,内联的事件处理函数没有必要重新生成,这样会产生不必要的内存开销和性能损耗。所以可以将内联事件处理函数缓存起来,在下一次执行渲染函数的时候,直接从缓存中获取。
  4. Block Tree:解决的是跳过静态节点比较的问题。
  5. 补丁标记:能够做到即便动态节点进行比较,也只比较有变化的部分的效果。

-EOF-

Released under the MIT License.