Vue3 是如何通过编译优化提升框架性能的?

简介: Vue3 是如何通过编译优化提升框架性能的?

Vue3 通过编译优化,极大的提升了它的性能。本文将深入探讨 Vue3 的编译优化的细节,了解它是如何提升框架性能的。

编译优化

编译优化指的是:编译器将模板编译为渲染函数的过程中,尽可能多地提取关键信息,用于指导生成最优代码的过程

编译优化的策略和具体实现,是由框架的设计思路所决定的,不同框架有不同思路,因此优化策略也是不同的

但优化方向基本一致,尽可能的区分动态内容和静态内容,针对不同的内容,采用不同的优化策略。

优化策略


Vue 作为组件级的数据驱动框架,当数据变化时,Vue 只能知道具体的某个组件发生了变化,但不知道具体是哪个元素需要更新。因此还需要对比新旧两棵 VNode 树,一层层地遍历,找出变化的部分,并进行更新。

但其实使用模板描述的 UI,结构是非常稳定的,例如以下代码:

html

复制代码

<template>
  <div class="container">
      <h1>helloh1>
      <h2>{{ msg }}h2>
  div>
template>

在这段代码中,唯一会发生变化的,就只有 h2 元素,且只会是内容发生变化,它的 attr 也是不会变化的。

如果对比新旧两颗 VNode 树,会有以下步骤:

  1. 比对 div
  2. 比对 div 的 children,使用 Diff 算法,找出 key 相同的元素,并一一进行比对
  1. 比对 h1 元素
  2. 比对 h2 元素

在对比完之后,发现 h2 元素的文本内容改变了,然后 Vue 会对 h2 的文本内容进行更新操作。

但实际上,只有 h2 元素会改变,我们如果可以只比对 h2 元素,然后找到它变化的内容,进行更新。

更进一步,其实 h2 只有文本会改变,只比对 h2 元素的文本内容,然后进行更新,这样就可以极大提升性能。

标记元素变化的部分


为了对每个动态元素的变化内容进行记录,需要引入 patchFlag 的概念

patchFlag


patchFlag 用于标记一个元素中动态的内容,它是 VNode 中的一个属性。

还是这个例子:


<template>
  <div>
      <h1>helloh1>
      <h2>{{ msg }}h2>
  div>
template>

加入 patchFlag 后的 h2 VNode 为:


{ type: 'h2', children: ctx.msg, patchFlag: 1 }

patchFlag 为 1,代表这个元素的 Text 部分,会发生变化。

注意:patchFlag 是一个 number 类型值,记录当前元素的变化的部分

PatchFlag 是 Typescript 的 Enum 枚举类型

下面是 PatchFlag 的部分枚举定义


export const enum PatchFlags {
  // 代表元素的 text 会变化
  TEXT = 1,
  // 代表元素的 class 会变化
  CLASS = 1 << 1,
  // 代表元素的 style 会变化
  STYLE = 1 << 2,
  // 代表元素的 props 会变化
  PROPS = 1 << 3,
  // ...
}

patchFlag === PatchFlags.TEXT,即 patchFlag === 1  时,代表元素的 Text 会变化。


patchFlag 使用二进制进行存储,每一位存储一个信息。如果 PatchFlag 第一位为 1,就说明 Text 是动态的,如果第二位为 1,就说明 Class 是动态的。

如果一个元素既有 Text 变化,又有 Class 变化,patchFlag 就为 3

PatchFlag.TEXT | PatchFlagCLASS1 | 2 ,1 二进制是 01,2 的二进制是 10,按位或的结果为 11,即十进制的 3。

计算过程如下:


1686403085475.png

有了这样的设计,我们可以根据每一位是否为 1,决定是否决定执行对应内容的更新

使用按位与 & 进行判断,具体过程如下:

1686403068555.png

伪代码如下:


function patchElement(n1, n2){
    if(n2.patchFlag > 0){
        // 有 PatchFlag,只需要更新动态部分
        if (patchFlag & PatchFlags.TEXT) {
            // 更新 class
        }
        if (patchFlag & PatchFlags.CLASS) {
            // 更新 class
        }
        if (patchFlag & PatchFlags.PROPS) {
            // 更新 class
        }
        ...
    } else {
        // 没有 PatchFlag,全量比对并更新
    }
}
当元素有 patchFlag 时,就只更新 patchFlag 对应的部分即可。
  • 如果没有 patchFlag,则将新老 VNode 全量的属性进行比对,找出差异并更新

为了能生成 dynamicChildrenpatchFlag,就需要编译器的配合,在编译时分析出动态的元素和内容

如何生成 patchFlag

由于模板结构非常稳定,很容易判断出模板的元素是否为动态元素,且能够判断出元素哪些内容是动态的

还是这个例子:

html

复制代码

<template>  <div>      <h1>helloh1>      <h2>{{ msg }}h2>  div>template>

Vue 编译器会生成如下的代码(并非最终生成的代码):


import { ref, createVNode } from 'vue'
const __sfc__ = {
    __name: 'App',
    setup() {
        const msg = ref('Hello World!')
        // 在 setup 返回编译后渲染函数
        return () => {
            return createVNode("div", { class: "container" }, [
                createVNode("h1", null, "hello"),
                createVNode("h2", null, msg.value, 1 /* TEXT */)
            ])
        }
    }
}

createVNode 函数,其实就是 Vue 提供的渲染函数 h,只不过它比 h 多传了 patchFlag 参数

对于动态的元素,在创建 VNode 的时候,会多传一个 patchFlag 参数,这样生成的 VNode,也就有了 patchFlag 属性,就代表该 VNode 是动态的。


记录动态元素


从上一小节我们可以知道,有 patchFlag 的元素,就是动态的元素,那如何对它们进行收集和记录呢?


为了实现上述目的,我们需要引入 Block(块)的概念


Block


Block 是一种特殊的 VNode,它可以负责收集它内部的所有动态节点

Block 比普通的 VNode 多了 dynamicChildren 属性,用于存储内部所有动态子节点。

还是这个例子:


<template>
  <div>
      <h1>helloh1>
      <h2>{{ msg }}h2>
  div>
template>

h1 的 VNode 为:


const h1 = { type: 'h1', children: 'hello' }

h2 的 VNode 为:


const h2 = { type: 'h2', children: ctx.msg, patchFlag: 1 }

div 的 VNode 为:


const vnode = {
    type: 'div',
    children: [
        h1,
        h2
    ],
    dynamicChildren: [
        h2  // 动态节点,会被存储在 dynamicChildren
    ],
}

这里的 div 就是 Block,实际上,Vue 会把组件内的第一个元素作为 Block

Block 更新


动态节点的 VNode,会被按顺序存储 Block 的 dynamicChildren

  • 存储在 dynamicChildren,是为了可以只对这些元素进行比对,跳过其他静态元素
  • dynamicChildren 只存储在 Block,不需要所有 VNode 都有 dynamicChildren,因为仅仅通过 Block dynamicChildren 就能找到其内部中所有的动态元素
  • 按顺序,即旧 VNode 的 dynamicChildren 和 新 VNode 的 dynamicChildren元素是一一对应的,这样的设计就不需要使用 Diff 算法,从新旧 VNode 这两个 children 数组中,找到对应(key 相同)的元素

那我们更新组件内元素的算法,可以是这样的:


// 传入两个元素的旧 VNode:n1 和新 VNode n2,
// patch 是打补丁的意思,即对它们进行比较并更新
function patchElement(n1, n2){
    if (n2.dynamicChildren) {
        // 优化的路径
        // 直接比对 dynamicChildren 就行
        patchBlockChildren(n1.dynamicChildren, n2.dynamicChildren)
    } else {
        // 全量比对
        patchChildren(n1, n2)
    }
}

patchBlockChildren 的大概实现如下:


// 对比新旧 children(是一个 VNode 的数组),并进行更新dynamicChildren
function patchBlockChildren(oldDynamicChildren, dynamicChildren){
    // 按顺序一一比对即可
    for (let i = 0; i < dynamicChildren.length; i++) {
        const oldVNode = oldDynamicChildren[i]
        const newVNode = dynamicChildren[i]
        // patch 传入新旧 VNode,然后进行比对更新
        patch(oldVNode, newVNode)
    }
}

直接按顺序比较 dynamicChildren,好像很厉害,但这样真的没问题吗?

其实是有问题的,但是能解决。

dynamicChildren 能按顺序进行比较的前提条件,是要新旧 VNode 中, dynamicChildren 的元素必须能够一一对应。那会不会存在不一一对应的情况呢?

答案是会的。

例如 v-if ,我们稍微改一下前面的例子(在线体验地址):


<template>
  <div>
      <h1 v-if="!msg">helloh1>
      <p v-else>
          <h2 >{{ msg }}h2>
      p>
  div>
template>

假如 msg 从 undefined 变成了 helloWorld

按我们上一小节所受的,旧的 VNode 的 dynamicChildren 为空(没有动态节点),新的 dynamicChildren 则是为h2

这种情况, v-if/v-else  让模板结构变得不稳定导致 dynamicChildren 不能一一对应。那要怎么办呢?

解决办法也很简单,v-if/v-else 的元素也作为 Block,这样就会得到一颗 Block 树。

1686402818698.png

Block 会作为动态节点,被 dynamicChildren 收集

例如:当  msg 为 undefined,组件内元素的 VNode 如下:


const vnode = {
    type: 'div',
    key: 0, // 这里新增了 key
    children: [
        h1
    ],
    dynamicChildren: [
        h1  // h1 是 Block(h1 v-if),会被存储在 dynamicChildren
    ],
}

当  msg 为不为空时,组件内元素的 VNode 如下:


const vnode = {
    type: 'div',
    key: 0,  // 这里新增了 key
    children: [
        h1
    ],
    dynamicChildren: [
        p // p 是 Block(p v-else),会被存储在 dynamicChildren
    ],
}

对于 Block(div) 来说,它的 dynamicChildren 是稳定的,里面的元素仍然是一一对应,因此可以快速找到对应的 VNode。

v-if/v-else 创建子 Block 的时候,会为子 Block 生成不同 key。在该例子中, Block(h1 v-if)Block(p v-else) 是对应的一组 VNode/Block,它们的 key 不同,因此在更新这两个 Block 时,Vue 会将之前的卸载,然后重新创建元素。

这种解决方法,其核心思想为:将不稳定元素,限制在最小的范围,让外层 Block 变得稳定

这样做有以下好处:

  • 保证稳定外层 Block 能继续使用优化的更新策略,
  • 在不稳定的内层 Block 中实施降级策略,只进行全量更新比对。

同样的,v-for 也会引起模板不稳定的问题,解决思路,也是将 v-for 的内容单独作为一层 Block,以保证外部 dynamicChildren 的稳定性。


如何创建 Block


只需要把有 patchFlag 的元素收集到 dynamicChildren 数组中即可,但如何确定 VNode 收集到哪一个 Block 中呢?

还是这个例子:


<template>
  <div>
      <h1>hello</h1>
      <h2>{{ msg }}</h2>
  </div>
</template>

Vue 编译器会生成如下的代码(并非最终代码):


import { ref, createVNode, openBlock } from 'vue'
const __sfc__ = {
    __name: 'App',
    setup() {
        const msg = ref('Hello World!')
        // 在 setup 返回编译后渲染函数
        return () => {
            return (
                // 新增了 openBlock
                openBlock(),
                // createVNode 改为了 createBlock
                createBlock("div", { class: "container" }, [
                    createVNode("h1", null, "hello"),
                    createVNode("h2", null, msg.value, 1 /* TEXT */)
                ]))
        }
    }
}

与上一小节相比,有以下不同:

  • 新增了 openBlock
  • createVNode 改为了 createBlock

由于 Block 是一个范围,因此需要 openBlockcloseBlock 去划定范围,不过我们看不到 closeBlock ,是因为 closeBlockcreateBlock 函数内被调用了。

处于 openBlockcloseBlock(或者 createBlock) 之间的元素,都会被收集到当前的 Block 中

我们来看一下 render 函数的执行顺序:

  1. openBlock初始化 currentDynamicChildren 数组
  2. createVNode,创建 h1 的 VNode
  3. createVNode,创建 h2 的 VNode,这个是动态元素,将 VNode push 到 currentDynamicChildren
  4. createBlock,创建 div 的 VNode,currentDynamicChildren 设置为 dynamicChildren
  • createBlock 中调用 closeBlock

值得注意的是,内层的 createVNode 是先执行, createBlock 是后执行的,因此能收集 openBlockcloseBlock  之间的动态元素 VNode

其中 openBlockcloseBlock 的实现如下:


// block 可能会嵌套,当发生嵌套时,用栈保存上一层收集的内容
// 然后 closeBlock 时恢复上一层的内容
const dynamicChildrenStack = []
// 用于存储当前范围中的动态元素的 VNode
let currentDynamicChildren = null
function openBlock(){
    currentDynamicChildren = []
    dynamicChildrenStack.push(currentDynamicChildren)
}
// 在 createBlock 中被调用
function closeBlock(){``
    currentDynamicChildren = dynamicChildrenStack.pop()
}

因为 Block 可以发生嵌套,因此要用栈存起来。openBlock 的时候初始化并推入栈,closeBlock 的时候恢复上一层的 dynamicChildren

createVnode 的代码大致如下:


function createVnode(tag, props, children, patchFlags){
    const key = props && props.key
    props && delete props.key
    const vnode = {
        tag,
        props,
        children,
        key,
        patchFlags
    }
    // 如果有 patchFlags,那就记录该动态元素的 Vnode
    if(patchFlags){
        currentDynamicChildren.push(vnode)
    }
    return vnode
}

createBlock 的代码大致如下:


function createBlock(tag, props, children){
    // block 本质也是一个 VNode
    const vnode = createVNode(tag, props, children)
    vnode.dynamicChildren = currentDynamicChildren
    closeBlock()
    // 当前 block 也会收集到上一层 block 的 dynamicChildren 中
    currentDynamicChildren.push(vnode)
    return vnode
}

其他编译优化手段


静态提升


仍然是这个例子(在线预览):


<template>
  <div>
      <h1>hello</h1>
      <h2>{{ msg }}</h2>
  </div>
</template>

实际上会编译成下图:

1686402659486.png

与我们前面小节不同的是,编译后的代码,会将静态元素的 createVNode 提升,这样每次更新组件的时候,就不会重新创建 VNode,因此每次拿到的  VNode 的引用相同,Vue 渲染器就会直接跳过其渲染


预字符串化


在线例子预览


<template>
  <div>
      <h1>hello</h1>
    <h1>hello</h1>
    <h1>hello</h1>
    <h1>hello</h1>
    <h1>hello</h1>
    <h1>hello</h1>
    <h1>hello</h1>
    <h1>hello</h1>
    <h1>hello</h1>
    <h1>hello</h1>
  </div>
</template>

1686402637206.png

如果模板中包含大量连续静态的标签节点,会将这些静态节点序列化为字符串,并生成一个 Static 的 VNode

这样的好处是:

  • 大块静态资源可以直接通过 innerHTML 设置,性能更佳
  • 减少创建大量的 VNode
  • 减少内存消耗

编译优化能用于 JSX 吗


目前 JSX 没有编译优化。

我在《浅谈前端框架原理》中谈到过:

  • 模板基于 HTML 语法进行扩展,其灵活性不高,但这也意味着容易分析
  • 而 JSX 是一种基于 ECMAScript 的语法糖,扩充的是 ECMAScript 的语法,但 ECMAScript 太灵活了,难以实现静态分析

例如:js 的对象可以复制、修改、导入导出等,用 js 变量存储的 jsx 内容,无法判断是否为静态内容,因为可能在不知道哪个地方就被修改了,无法做静态标记。

但也并不是完全没有办法,例如可以通过约束 JSX 的灵活性,使其能够被静态分析,例如 SolidJS。


总结


在本文中,我们首先讨论了编译优化的优化方向:尽可能的区分动态内容和静态内容

然后具体到 Vue 中,就是从模板语法中,分离出动态和静态的元素,并标记动态的元素,以及其动态的部分

当我们标记动态的内容后,Vue 就可以配合渲染器,快速找到并更新动态的内容,从而提升性能

接下来介绍如何实现这一目的,即【如何标记元素变化的部分】和【如何记录动态的元素

最后还稍微介绍一些其他的编译优化手段,以及解释了为什么 JSX 难以做编译优化。

如果这篇文章对您有所帮助,可以点赞加收藏👍,您的鼓励是我创作路上的最大的动力。也可以关注我的公众号订阅后续的文章:Candy 的修仙秘籍(点击可跳转)


目录
相关文章
|
5天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
15天前
|
JavaScript 前端开发 开发者
Vue 3中的Proxy
【10月更文挑战第23天】Vue 3中的`Proxy`为响应式系统带来了更强大、更灵活的功能,解决了Vue 2中响应式系统的一些局限性,同时在性能方面也有一定的提升,为开发者提供了更好的开发体验和性能保障。
35 7
|
15天前
|
JavaScript 前端开发 开发者
前端框架对比:Vue.js与Angular的优劣分析与选择建议
【10月更文挑战第27天】在前端开发领域,Vue.js和Angular是两个备受瞩目的框架。本文对比了两者的优劣,Vue.js以轻量级和易上手著称,适合快速开发小型到中型项目;Angular则由Google支持,功能全面,适合大型企业级应用。选择时需考虑项目需求、团队熟悉度和长期维护等因素。
21 1
|
16天前
|
前端开发 数据库
芋道框架审批流如何实现(Cloud+Vue3)
芋道框架审批流如何实现(Cloud+Vue3)
38 3
|
15天前
|
JavaScript 数据管理 Java
在 Vue 3 中使用 Proxy 实现数据双向绑定的性能如何?
【10月更文挑战第23天】Vue 3中使用Proxy实现数据双向绑定在多个方面都带来了性能的提升,从更高效的响应式追踪、更好的初始化性能、对数组操作的优化到更优的内存管理等,使得Vue 3在处理复杂的应用场景和大量数据时能够更加高效和稳定地运行。
36 1
|
15天前
|
JavaScript 开发者
在 Vue 3 中使用 Proxy 实现数据的双向绑定
【10月更文挑战第23天】Vue 3利用 `Proxy` 实现了数据的双向绑定,无论是使用内置的指令如 `v-model`,还是通过自定义事件或自定义指令,都能够方便地实现数据与视图之间的双向交互,满足不同场景下的开发需求。
36 1
|
16天前
|
JavaScript 前端开发 API
前端框架对比:Vue.js与Angular的优劣分析与选择建议
【10月更文挑战第26天】前端技术的飞速发展让开发者在构建用户界面时有了更多选择。本文对比了Vue.js和Angular两大框架,介绍了它们的特点和优劣,并给出了在实际项目中如何选择的建议。Vue.js轻量级、易上手,适合小型项目;Angular结构化、功能强大,适合大型项目。
16 1
|
17天前
|
前端开发 JavaScript
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
简记 Vue3(一)—— setup、ref、reactive、toRefs、toRef
|
5天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
5天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。