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

编译优化

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

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

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

优化策略

Vue 作为组件级的数据驱动框架,当数据变动时,Vue 只能晓得具体的某个组件产生了变动,但不晓得具体是哪个元素须要更新。因而还须要比照新旧两棵 VNode 树,一层层地遍历,找出变动的局部,并进行更新。

但其实应用模板形容的 UI,构造是十分稳固的,例如以下代码:

<template>  <div class="container">      <h1>hello</h1>      <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>hello</h1>      <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。

计算过程如下:

有了这样的设计,咱们能够依据每一位是否为 1,决定是否决定执行对应内容的更新

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

伪代码如下:

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

因为模板构造十分稳固,很容易判断出模板的元素是否为动静元素,且可能判断出元素哪些内容是动静的

还是这个例子:

<template>  <div>      <h1>hello</h1>      <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>hello</h1>      <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 的数组),并进行更新function patchBlockChildren(oldDynamicChildren, oldDynamicChildren){    // 按程序一一比对即可    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">hello</h1>      <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 树。

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 ,是因为 closeBlock 间接在 createBlock 函数内被调用了。

处于 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 = []// 用于存储以后范畴中的动静元素的 VNodelet currentDynamicChildren = nullfunction 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>

实际上会编译成下图:

与咱们后面大节不同的是,编译后的代码,会将动态元素的 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>

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

这样的益处是:

  • 大块动态资源能够间接通过 innerHTML 设置,性能更佳
  • 缩小创立大量的 VNode
  • 缩小内存耗费

编译优化能用于 JSX 吗

目前 JSX 没有编译优化。

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

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

例如:js 的对象能够复制、批改、导入导出等,用 js 变量存储的 jsx 内容,无奈判断是否为动态内容,因为可能在不晓得哪个中央就被批改了,无奈做动态标记。

但也并不是齐全没有方法,例如能够通过束缚 JSX 的灵活性,使其可能被动态剖析,例如 SolidJS。

总结

在本文中,咱们首先探讨了编译优化的优化方向:尽可能的辨别动静内容和动态内容

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

当咱们标记动静的内容后,Vue 就能够配合渲染器,疾速找到并更新动静的内容,从而晋升性能

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

最初还略微介绍一些其余的编译优化伎俩,以及解释了为什么 JSX 难以做编译优化。

如果这篇文章对您有所帮忙,能够点赞加珍藏,您的激励是我创作路上的最大的能源。也能够关注我的公众号订阅后续的文章:Candy 的修仙秘籍(点击可跳转)