vue3 提速小巧思🚀,值得一提的编译优化!

Vue 卷了一代又一代,可谓是越卷越快,从 Vue2Vue3 更是完成了一个大的飞跃!而 Vue3 之所以这么快,很大一部分归功于它做的一系列 编译优化 策略。

我们今天就来看看它究竟修炼了什么秘籍来提升速度。

从模板到真实 DOM

在讲解编译优化之前,我们先来盘一盘 Vue 是如何把 我们写的模板变成页面上的真实 DOM 的。

下面是一个组件对应的模板:

html 复制代码
<template>
  <div>
    <h1>我是静态文本</h1>
    <h2>{{ dynamicText }}</h2>
  </div>
</template>

模板编译

Vue2 时代,这段模板会被编译器编译成类似下面这样的渲染函数:

js 复制代码
// 渲染函数
export function render(ctx) {
  return (createElementVNode("div", null, [
    createElementVNode("h1", null, "我是静态文本"),
    // 新增一个参数 1
    createElementVNode("h2", null, ctx.dynamicText, 1 /* TEXT */)
  ]))
}

这里的 createElementVNode 方法简单来说就是:创建并返回虚拟 DOM 的方法

js 复制代码
function createElementVNode(tag, props, children) {
  const vnode = {
    tag,
    props,
    children,
    key: props.key
  }
  return vnode
}

想了解 Vue3 编译原理的小伙伴,可以参考我之前写的: 《# Vue3 编译原理直通车💥------parser 篇》

运行时

接下来,如果我们的代码是在浏览器环境运行的话,上面的渲染函数会被交给浏览器;

这时候就是 Vue 运行时 的地盘了,上面的渲染函数会被执行最终生成如下的虚拟 DOM :

js 复制代码
// 旧 vnode
const vnode = {
  tag: 'div',
  children: [
    {
      tag: 'h1',
      children: '我是静态文本'
    },
    {
      tag: 'h2',
      children: ctx.dynamicText
    }
  ]
}

Vue渲染器 (renderer) 会负责调用浏览器平台相关的 API ,把 虚拟 DOM 渲染成真实的 DOM 元素

而当 dynamicText 变量对应的内容发生了改变之后,由于这是个响应式数据;它会导致这个 **组件的渲染函数被再次执行,生成新的虚拟 DOM **;

这时候 Vuediff 算法 开始发挥作用,通过层层比较新旧虚拟 DOM 节点,diff 算法 找到了差异部分,也就是变量 dynamicText 所在的部分。

最后,渲染器 (renderer) 再次调用浏览器相关 API 将差异的部分更新到页面上。

这么一盘,不知道细心的小伙伴们有没有发现问题所在?

在我们给出的这段模板中,实际上 只有 dynamicText 所在的部分是可能发生变化的 ,但是在进行 diff 算法 时,却需要层层向下进行比较;

如果模板中有大量静态的节点,只有一个动态的部分,这样逐层的比较无疑是十分浪费性能的。

Vue3 中是如何解决这个问题的呢?

靶向更新

Vue3 的解决思路是:给模板中动态的部分加上标识,并将它们提取出来存放在虚拟 DOM 中独立的一块区域

比如像这样:

JS 复制代码
const vnode = {
  tag: 'div',
  children: [
    { tag: 'h1', children: '我是静态文本' },
    {
      tag: 'h2',
      children: ctx.dynamicText,
      // 增加一个 patchFlag 标识
      patchFlag: 1
    }
  ],
  // 专门用于存放动态节点
  dynamicChildren: [
    { tag: 'h2', children: ctx.dynamicText, patchFlag: 1 }
  ]
}

这里的 patchFlag 是个 number 类型,不同的数值代表了不同的含义:

js 复制代码
const patchFlags = {
  TEXT: 1, // 表示动态文本内容
  CLASS: 2, // 表示动态的 class
  STYLE: 3, // 表示动态的 style
  PROPS: 4 // 表示动态的 props
  // 省略...
}

这样一来,在渲染器进行 diff 对比新旧 vnode 时就 只需要对比存放了动态节点的 dynamicChildren 数组就可以了

同时由于动态节点上存在着 patchFlag 标识,明确指出了当前节点需要更新的部分,从而达到 靶向更新 的效果。

在具体实现上,Vue 首先在 编译模板时就会收集相关的动态节点信息,将动态节点的相关信息体现在最终生成的渲染函数上:

js 复制代码
export function render(ctx) {
  return (createElementVNode("div", null, [
    createElementVNode("h1", null, "我是静态文本"),
    // 新增一个参数 1
    createElementVNode("h2", null, ctx.dynamicText, 1 /* TEXT */)
  ]))
}

可以看到,新的渲染函数中给 createElementVnode 方法传递第 4 个参数:数字 1;它表示这个节点是一个有动态文本内容的节点;

现在已经有了动态节点的标识 patchFlag,接下来就要考虑如何将这些动态节点收集到根节点 divvnode 下的 dynamicChildren 数组中了。

这里有一点需要注意 ------

上面渲染函数是 由内向外执行 的,也就是说 h1h2 节点的 createElementVnode 会先执行,最后才到 div 节点对应的 createElementVnode 方法;

divcreateElementVNode 执行时,动态节点 h2createElementVNode 方法已经执行完毕 ,那么 div 就无法将对应的节点收集到自身的 dynamicChildren 数组中。

为了解决这个问题,Vue3 使用了一个 栈的结构 配合 openBlock 方法来 临时储存内层的动态节点

js 复制代码
// 创建一个临时栈以及 openBlock 方法
// 临时栈 用于还原渲染函数执行时的层级关系
const dynamicChildrenStack = []
// 用于储存当前的动态节点
let currentDynamicChildren = null

function openBlock() {
  const currentDynamicChildren = []
  dynamicChildrenStack.push(currentDynamicChildren)
}

同时修改 createElementVNode 方法:

js 复制代码
// 增加第四个入参 patchFlags
function createElementVNode(tag, props, children, patchFlags) {
  const vnode = {
    tag,
    props,
    children,
    key: props.key
  }
  // 如果当前节点是个动态节点,就将其 push 进 currentDynamicChildren 中
  if (typeof patchFlags !== 'undefined' && currentDynamicChildren) {
    currentDynamicChildren.push(vnode)
  }
  return vnode
}

针对那些需要收集动态节点的 vnode,对应的创建虚拟节点的方法也需要改变:

js 复制代码
// 增加一个 closeBlock 方法,用于将当前的动态节点集合从栈中弹出
function closeBlock() {
  currentDynamicChildren = dynamicChildrenStack.pop()
}

// 创建一个带有 dynamicChildren 的 vnode
function createElementBlock() {
  const block = createElementVnode()
  block.dynamicChildren = currentDynamicChildren
  // 最后调用 closeBlock 方法关闭当前动态节点的收集
  closeBlock()
  return block
}

最后修改渲染函数,在创建 vnode 之前先调用 openBlock 方法,并将 div 节点对应的 createElementVNode 方法修改为 createElementBlock

js 复制代码
export function render(ctx) {
  // 先调用 openBlock 方法来创建临时栈
  // 并将原本 div 的 createElementVNode 方法修改为 createElementBlock
  return (openBlock(), createElementBlock("div", null, [
    createElementVNode("h1", null, "我是静态文本"),
    // 新增一个参数 1
    createElementVNode("h2", null, ctx.dynamicText, 1 /* TEXT */)
  ]))
}

这样一来就能够将 h2 对应的动态节点正确的收集到根节点 divdynamicChildren 中了。

而这种 带有 dynamicChildrenvnode 则被称为块 (Block)

静态提升

说完了 Vue3 是如何提取动态内容来实现靶向更新,我们再来看看 静态提升

还是原先那段模板,正常情况下它对应的渲染函数如下:

js 复制代码
export function render(ctx) {
  return (openBlock(), createElementBlock("div", null, [
    createElementVNode("h1", null, "我是静态文本"),
    // 新增一个参数 1
    createElementVNode("h2", null, ctx.dynamicText, 1 /* TEXT */)
  ]))
}

而这个渲染函数中,h1 标签对应的 createElementVNode 逻辑是不会发生变化的;

因此,我们可以 将这段逻辑抽离到 render 函数之外,避免由于渲染函数重新执行导致这段逻辑再次执行,从而造成浪费

js 复制代码
// 这里将创建 vnode 的逻辑提取到 render 函数之外
const hoist1 = createElementVNode("h1", null, "我是静态文本")
export function render(ctx) {
  return (openBlock(), createElementBlock("div", null, [
    hoist1, // 这里使用 h1 创建 vnode 逻辑的引用
    createElementVNode("h2", null, ctx.dynamicText, 1 /* TEXT */)
  ]))
}

除了针对整个节点的静态提升之外,如果节点的内容是动态的,但是 属性是纯静态的,也可以进行属性的静态提升。

如下模板:

html 复制代码
<template>
  <h2
    class="hello"
    style="color:red"
    id="1"
  >
   {{ dynamicText }}
  </h2>
</template>

静态提升后的渲染函数:

js 复制代码
// 针对属性进行静态提升
const hoist1 = {
  class: "hello",
  style: {"color":"red"},
  id: "1"
}

export function render(ctx) {
  return (openBlock(), createElementBlock("template", null, [
    // 这里的第二个参数变为静态引用
    createElementVNode("h2", hoist1, ctx.dynamicText, 1 /* TEXT */)
  ]))
}

预字符串化

基于静态提升 Vue3 还做了更进一步的优化 ------ 预字符串化。

所谓预字符串化就是:将静态提升后的节点预先序列化为字符串

比如下面这段模板:

html 复制代码
<template>
  <h1>text</h1>
  // ... 中间省略 8 个 <h1>text</h1>
  <h1>text</h1>
</template>

当上面这样的 连续的静态内容超过了一定数量(10 个),那么 Vue3 就会将这些节点合并为一个静态 vnode 调用

js 复制代码
// 将 <h1>text</h1> 合并成一个静态节点的创造
const hoist1 = /*#__PURE__*/createStaticVNode("<h1>text</h1> //... <h1>text</h1>", 10)
const hoist11 = [
  hoist1
]

export function render(_ctx) {
  return (openBlock(), createElementBlock("template", null, hoist11))
}

这样一来就可以 避免创建虚拟 DOM 带来的开销,同时这部分静态内容可以通过 innerHTML 直接设置,对性能也有一定的提升

缓存内联事件

除了对静态节点的提升,Vue3 还对于 内联事件 做了缓存。

vue 复制代码
<template>
  <button @click="num++">点我</button>
</template>

上面这段模板,在没有对内联事件进行缓存时,编译后的渲染函数如下:

javascript 复制代码
export function render(ctx) {
  return (openBlock(), createElementBlock("template", null, [
    createElementVNode("button", {
      // 创建一个内联函数
      onClick: $event => (ctx.num++)
    }, "点我", 8 /* PROPS */, ["onClick"])
  ]))
}

很显然,每次 render 函数重新执行都会重新创建一个 onClick 函数;

这样一来,由于 onClick 函数前后的指针不一致,渲染器会认为 button 的属性被更新了,从而去更新 button 按钮,造成额外的开销

因此 Vue3针对内联事件进行了缓存

具体做法就是:将这些内联事件缓存在一个数组 cache 中,并渲染函数执行时优先从缓存中读取事件处理函数

javascript 复制代码
// 给渲染函数新增一个 cache 参数
export function render(ctx, cache) {
  return (openBlock(), createElementBlock("template", null, [
    createElementVNode("button", {
      // 优先从缓存中读取事件处理函数
      onClick: cache[0] || (cache[0] = $event => (ctx.num++))
    }, "点我")
  ]))

这里传给 render 函数的第二个参数 cache 就是用于缓存内联事件的数组,它与 ctx 一样被挂在对应的 组件实例 上。

缓存虚拟 DOM

如果模板中使用了 v-once 指令,Vue3 还可以实现对虚拟 DOM 的缓存。

Vuev-once 是用于 将元素或组件标记为只渲染一次的静态内容

假设有如下模板,在模板上使用了 v-once 指令:

html 复制代码
<template>
  <div v-once>{{ text }}</div>
</template>

上面的模板编译成渲染函数后:

js 复制代码
export function render(ctx, cache) {
  return (openBlock(), createElementBlock("template", null, [
    cache[0] || (
      // 阻止 Block 追踪
      setBlockTracking(-1),
      // 创建对应虚拟节点并缓存
      cache[0] = createElementVNode("div", null, [
        createTextVNode(ctx.text, 1 /* TEXT */)
      ]),
      // 恢复 Block 追踪
      setBlockTracking(1),
      // 从缓存中读取对应虚拟节点并返回
      cache[0]
    )
  ]))
}

从上面的代码中可以看出,虚拟 DOM 的缓存思路与内联事件的类似 ------ 都是 优先从缓存中读取结果,没有读取到的情况下再进行重新创建

而值得一提的是:由于这段虚拟节点已经被缓存了是不会变化的,因此在父节点进行 块(Block) 收集时 它不应该被收集到 dynamicChildren

所以可以看到,代码中使用了一个 setBlockTracking(-1) 方法来阻止它被收集,并在其创建虚拟 DOM 的逻辑完成后,使用 setBlockTracking(1) 方法来恢复收集。

总结

以上就是 Vue3 针对编译优化所做的全部内容。

各位小伙伴还可以戳这里来帮助理解编译优化:vue3 模板在线编译结果

这个网站可以很直观的看到经过静态提升、内联事件缓存等优化后的编译结果。

That's all 🌋🌋🌋

相关推荐
golitter.1 分钟前
Ajax和axios简单用法
前端·ajax·okhttp
雷特IT21 分钟前
Uncaught TypeError: 0 is not a function的解决方法
前端·javascript
长路 ㅤ   43 分钟前
vite学习教程02、vite+vue2配置环境变量
前端·vite·环境变量·跨环境配置
EterNity_TiMe_1 小时前
【机器学习】智驭未来:探索机器学习在食品生产中的革新之路
人工智能·python·机器学习·性能优化·学习方法
亚里士多没有德7751 小时前
强制删除了windows自带的edge浏览器,重装不了怎么办【已解决】
前端·edge
micro2010141 小时前
Microsoft Edge 离线安装包制作或获取方法和下载地址分享
前端·edge
.生产的驴1 小时前
Electron Vue框架环境搭建 Vue3环境搭建
java·前端·vue.js·spring boot·后端·electron·ecmascript
awonw1 小时前
[前端][easyui]easyui select 默认值
前端·javascript·easyui
老齐谈电商1 小时前
Electron桌面应用打包现有的vue项目
javascript·vue.js·electron
LIURUOYU4213081 小时前
vue.js组建开发
vue.js