Vue3 通过编译优化,极大的晋升了它的性能。本文将深入探讨 Vue3 的编译优化的细节,理解它是如何晋升框架性能的。
编译优化
编译优化指的是:编译器将模板编译为渲染函数的过程中,尽可能多地提取要害信息,用于领导生成最优代码的过程
编译优化的策略和具体实现,是由框架的设计思路所决定的,不同框架有不同思路,因而优化策略也是不同的
但优化方向基本一致,尽可能的辨别动静内容和动态内容,针对不同的内容,采纳不同的优化策略。
优化策略
Vue 作为组件级的数据驱动框架,当数据变动时,Vue 只能晓得具体的某个组件产生了变动,但不晓得具体是哪个元素须要更新。因而还须要比照新旧两棵 VNode 树,一层层地遍历,找出变动的局部,并进行更新。
但其实应用模板形容的 UI,构造是十分稳固的,例如以下代码:
<template> <div class="container"> <h1>hello</h1> <h2>{{ msg }}</h2> </div></template>
在这段代码中,惟一会发生变化的,就只有 h2
元素,且只会是内容发生变化,它的 attr
也是不会变动的。
如果比照新旧两颗 VNode 树,会有以下步骤:
- 比对
div
比对
div
的 children,应用 Diff 算法,找出 key 雷同的元素,并一一进行比对- 比对
h1
元素 - 比对
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 | PatchFlagCLASS
,1 | 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 全量的属性进行比对,找出差别并更新
为了能生成 dynamicChildren
和 patchFlag
,就须要编译器的配合,在编译时剖析出动静的元素和内容
如何生成 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
,因为仅仅通过 BlockdynamicChildren
就能找到其外部中所有的动静元素- 按程序,即旧 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
是一个范畴,因而须要 openBlock
和 closeBlock
去划定范畴,不过咱们看不到 closeBlock
,是因为 closeBlock
间接在 createBlock
函数内被调用了。
处于 openBlock
和 closeBlock
(或者 createBlock
) 之间的元素,都会被收集到以后的 Block 中。
咱们来看一下 render
函数的执行程序:
openBlock
,初始化currentDynamicChildren
数组createVNode
,创立h1
的 VNodecreateVNode
,创立h2
的 VNode,这个是动静元素,将 VNode push 到currentDynamicChildren
createBlock
,创立div
的 VNode,将currentDynamicChildren
设置为dynamicChildren
- 在
createBlock
中调用closeBlock
- 在
值得注意的是,内层的createVNode
是先执行,createBlock
是后执行的,因而能收集openBlock
和closeBlock
之间的动静元素 VNode
其中 openBlock
和 closeBlock
的实现如下:
// 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 的修仙秘籍(点击可跳转)