HTML5
分享创造价值 合作实现共赢

HTML5

当前位置: 首页 > 新闻动态 > HTML5

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

发布时间:2023-02-23 11:04:05作者:顺晟科技点击:

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

编译优化

编译优化是指在编译器将模板编译成渲染函数时,提取尽可能多的关键信息来指导生成最优代码的过程。

框架的设计思想决定了编译优化的策略和具体实现。不同的框架有不同的思路,所以优化策略也不同。

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

优化策略

Vue是组件级的数据驱动框架。当数据发生变化时,Vue只能知道某个特定的组件发生了变化,而不知道哪个元素需要更新。所以需要对比新旧VNode树,逐层遍历,找出变化的部分,进行更新。

但实际上,模板描述的UI有一个非常稳定的结构,比如下面的代码:

template div class=' container ' h1 hello/h1 h2 { { msg } }/H2/div/template在这段代码中,唯一会改变的是H2元素,而且只有内容会改变,它的attr不会改变。

如果比较新旧VNode树,有以下步骤:

将div与div的子元素进行比较,使用Diff算法找出具有相同键的元素,并逐个进行比较。

h1元素和h2元素对比后,发现h2元素的文本内容发生了变化,然后Vue会更新h2的文本内容。

但实际上只有h2元素会发生变化。如果只能对比h2元素,那就找出它变化的内容,进行更新。

再者,其实只改变h2的文本,只比较h2元素的文本内容,然后更新,可以大大提高性能。

标记元素变化的部分

为了记录每个动态元素的变化,需要引入patchFlag的概念。

patchFlag

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

或者这个例子:

模板div h1 hello/h1 H2 { { msg } }/H2/div/template H2添加patchFlag后的VNode为:

{类型:' H2 '孩子:CTX。msg,patchflag: 1} Patchflag为1,表示这个元素的文本部分会发生变化。

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

PatchFlag是Typescript的Enum枚举类型。

以下是PatchFlag的一些枚举定义。

Export const enum PatchFlags {//表示元素的文本将改变TEXT=1,//表示元素的类将改变CLASS=1 1,//表示元素的样式将改变STYLE=1 2,//表示元素的props将改变PROPS=1 3,//.}当patchFlag===PatchFlags时。文本,即patchFlag===1,表示元素的文本会发生变化。

PatchFlag以二进制存储,每一位存储一条信息。如果PatchFlag的第一位为1,则表示文本是动态的,如果第二位为1,则表示类是动态的。

如果一个元素既有文本更改又有类更改,则patchFlag为3。

也就是PatchFlag。TEXT | PatchFlagCLASS,1 | 2,1是二进制的01,2是二进制的10,按位或的结果是11,也就是十进制的3。

计算过程如下:

通过这样的设计,我们可以根据每一位是否为1来决定是否更新相应的内容。

用按位AND来判断,具体过程如下:

伪代码如下:

函数patchelement (n1,n2) {if (n2。PatchFlag 0){//用patch flag只更新动态部分if(patch flag patch flags . text){//update class } if(patch flag patch flags . class){//update class } if(patch flag patch flags . props){//update class }.} else {//没有PatchFlag,比较更新总量}}当一个元素有patchFlag时,只能更新patchFlag对应的部分。如果没有patchFlag,对比新旧VNode的全属性,找出差异并更新。为了生成dynamicChildren和patchFlag,编译器需要在编译时合作分离出动态元素和内容。

如何生成 patchFlag

因为模板结构非常稳定,所以很容易判断模板的元素是否是动态的,元素的哪些内容是动态的。

或者这个例子:

template div h1 hello/h1 H2 { { msg } }/H2/div/template vue编译器将生成以下代码(不是最终代码):

从' vue'const __sfc__={ __name: 'App 'setup(){ const msg=ref(' Hello World!)//返回编译后的渲染函数return()={ return createvnode(' div '{class:' container'},[createvnode ('h1 'null,' hello '),createvnode ('h2 'null,Msg.value,1/* text */)]} } create vnode函数其实就是Vue提供的渲染函数H,只不过它传递的patchFlag参数比H多。

对于动态元素,在创建VNode时,将传递一个附加的patchFlag参数,这样生成的VNode将具有patchFlag属性,这意味着VNode是动态的。

记录动态元素

从上一节我们可以知道,带有patchFlag的元素是动态元素,那么如何采集记录呢?为了实现上述目标,我们需要引入块的概念。

Block

Block是一个特殊的VNode,可以把所有的动态节点都收集在里面。

Block比普通的VNode有更多的dynamicChildren属性,用来存储里面所有的动态子节点。

或者这个例子:

模板div h1 hello/h1 H2 { { msg } }/H2/div/template h1的VNode为:

consth1={type:' h1 'children:' hello'} H2的VNode是:

Consth2={type:' h2 'children: ctx.msg,patch flag:1 } div的VNode为:

Constvnode={type:' div 'children: [h1,H2],dynamic children:[H2//动态节点,将存储在dynamicChildren ],}这里的div就是块。实际上,Vue将组件中的第一个元素作为块。

Block 更新

动态节点的VNode将按顺序存储在块的dynamicChildren中。

它存储在dynamicChildren中,因此只能比较这些元素,而跳过其他静态元素。dynamicChildren只存储在Block中,并不是所有的VNode都必须有dynamicChildren,因为其中所有的动态元素只有通过Block dynamicChildren才能按顺序找到。即旧VNode的dynamicchildren和新VNode的dynamicChildren的元素是一一对应的,所以设计时不需要使用Diff算法,从新旧VNode的两个子数组中找到对应的(相同key)元素。那么我们更新组件中元素的算法如下:

//传入两个元素的旧VNode:n1和新VNode:n1。//patch的意思是打补丁,也就是比较它们,更新函数patchElement(n1,N2) {If (N2。dynamic children优化路径//直接对比动态子就可以了。PatchBlockChildren (n1。充满活力的孩子,N2。dynamic Children)} Else {//总比率对PatchBlockChildren (n1,N2)} PatchBlockChildren的近似实现如下:

//新旧子代比较(VNode的一个数组),更新函数patchblockchildren(旧动态子代){//逐个比较,以得到for(设I=0;i dynamicChildren.lengthI){ const old VNode=old dynamic Children[I]const new VNode=dynamic children[I]//Patch传入新老VNode,然后比较更新patch (old vnode,new vnode)}直接按顺序比较动态children看起来很厉害,但是这样真的可以吗?其实是有问题,但是可以解决。

要按顺序比较dynamicChildren的先决条件是dynamicChildren的元素必须能够在新旧VNode中相互对应。会不会没有一一对应?

答案是肯定的。

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

模板div h1 v-if='Msg ' hello/h1pv-else H2 { { MSG } }/H2/p/div/template如果MSG从undefined变成helloWorld,

根据上一节所学,旧VNode的dynamicChildren为空(没有动态节点),新的dynamicChildren为h2。

在这种情况下,v-if/v-else使得模板结构不稳定,导致了dynamicChildren的一一对应。那么我们该怎么办呢?

解决方法也很简单。让v-if/v-else的元素也作为一个块,你会得到一个块树。

块将被dynamicChildren作为动态节点收集。

例如,当msg未定义时,组件中元素的VNode如下:

ConstVNode={type:' div 'key: 0,//这里是新添加的Keychildren: [h1],DynamicChildren: [h1//h1是一个块(h1 v-if),将存储在DynamicChildren中]。}当msg不为空时,组件中元素的vnode如下:

Constvnode={type:' div 'key: 0,//Key children: [h1],Dynamic children:[p//p is Block(p v-else)并将存储在dynamicChildren]此处添加。}对于Block(div)来说,它的dynamicChildren是稳定的,里面的元素还是一一对应的,所以可以很快找到对应的VNode。

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

这种解决方案的核心思想是将不稳定元素限制在最小范围内,使外块稳定。

这具有以下优点:

保证稳定的外块可以继续使用优化的更新策略,在不稳定的内块实现降级策略,只比较完全更新。同样,v-for也会造成模板不稳定的问题。为了解决这个问题,v-for的内容也单独作为一层块来保证外部动态子体的稳定性。

如何创建 Block

只需要将带有patchFlag的元素收集到dynamicChildren数组中,但是如何确定将VNode收集到哪个块中呢?

或者这个例子:

template div h1 hello/h1 H2 { { msg } }/H2/div/template vue编译器将生成以下代码(不是最终代码):

从' vue'const __sfc__={ __name: 'App 'setup(){ const msg=ref(' Hello World!)//编译后的渲染函数return ()={return (//OpenBlock())增加,//CreateVNode改为CreateBlock CreateBlock ('div '{class:' container'},[createvnode ('h1 'null,' hello '),createvnode ('H2 'null,msg.value,1/* text */))} }与上一节相比有以下不同:

添加了OpenBlockcreateVNode,并将其更改为createBlock。因为Block是一个范围,所以我们需要openBlock和closeBlock来划定范围,但是我们看不到closeBlock,因为closeBlock是在createBlock函数中直接调用的。

openBlock和closeBlock(或createBlock)之间的元素将被收集到当前块中。

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

OpenBlock,初始化currentDynamicChildren数组createVNode,创建h1的VNodecreateVNode,创建h2的VNode,这是一个动态元素,VNode将VNode推送给currentdynamic children CreateBlock。创建div的VNode,并将currentDynamicChildren设置为DynamicChildren。

在createBlock中调用closeBlock值得注意的是,先执行内部createVNode,后执行createBlock,所以可以收集openBlock和closeBlock之间的动态元素VNode。openBlock和closeBlock的实现如下:

//块可能是嵌套的,发生嵌套时用栈保存上一层收集的内容//然后在关闭块时恢复上一层的内容const dynamicChildrenStack=[]///VNodelet currentdynamicshildren=null函数closeBlock()用于存储当前范围内的动态元素。{ current dynamic children=[]dynamic children。push(currentdynamicschildren)}//在createBlock { ` `中调用函数closeblock()。current dynamic children=dynamic children。Pop ()}因为块可以嵌套,所以应该存储在堆栈中。打开块时初始化并推栈,关闭块时恢复上层的dynamicChildren。

createVnode的代码大致如下:

函数createVnode(tag,props,children,patch flags){ const key=props props . key props delete props . key const vnode={ tag,props,children,key,PatchFlags} //如果有补丁标志,记录vnode if(补丁标志){ CurrentDynamicChildren。动态元素的Push (vnode)} Return vnode}代码如下:

函数create block (tag,props,children) {//block也是一个vnode const vnode=create vnode(tag,props,children)vnode . dynamicChildren=currentdynamic children closeBlock()//当前块也会被收集到上一个块的dynamic children中。CurrentDynamicChildren。push(vnode)Return vnode }

其他编译优化手段

静态提升

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

模板div h1 hello/h1 H2 { { msg } }/H2/div/template实际上会编译成下图:

与我们上一节不同的是,编译后的代码会提升静态元素的createVNode,这样就不会在每次更新组件的时候重新创建VNode,所以每次VNode引用都是一样的,Vue渲染器会直接跳过它的渲染。

预字符串化

在线示例预览

模板div h1 hello/h1 h1 hello/h1 h1 hello/h1 h1 hello/h1 h1 h1 hello/h1 h1 h1 hello/h1 h1 h1 hello/h1 h1 hello/h1 h1 hello/h1 h1 hello/h1/div/template

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

这样做的好处是:

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

编译优化能用于 JSX 吗

目前JSX没有编译优化。

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

模板是基于HTML语法扩展的,不太灵活,但也意味着容易分析。JSX是一个基于ECMAScript的语法糖,它扩展了ECMAScript的语法,但是ECMAScript过于灵活,无法实现静态分析。例如,js对象可以被复制、修改、导入和导出等。用JS变量存储的jsX内容不能判断为静态内容,因为它可能在某处被修改过,不能静态标记。

但也不是完全无奈。比如JSX可以通过限制其灵活性来进行静态分析,比如SolidJS。

总结

本文首先讨论编译优化的优化方向:尽可能区分动态内容和静态内容。

然后,在Vue中,从模板语法中分离出动态和静态元素,并对动态元素及其动态部分进行标记。

当我们标记动态内容时,Vue可以配合渲染器快速查找和更新动态内容,从而提高性能。

接下来,我们将介绍如何实现这一目标,即如何标记元素发生变化的部分,以及如何记录动态元素。

最后,简单介绍了其他一些编译优化方法,并解释了为什么JSX很难做编译优化。

如果这篇文章对你有帮助,可以赞一下,收藏起来。你们的鼓励是我创作道路上最大的动力。也可以关注我的微信官方账号订阅后续文章:糖果的修仙秘籍(点击跳转)。

TOP

QQ客服

18910140161