vue2、vue3区别虚拟dom和Slot性能提升的原理

来源:程序思维浏览:715次
Vue3核心的 Typescript, Proxy响应式, Composition解决代码反复横跳都有很棒的文章剖析了, 我总结一下虚拟 Dom部分把,并对比一下 React, vdom的重写也是vue3性能如此优秀的重要原因。

Vue3虚拟DOM篇

静态标记, upadte性能提升1.3~2倍, ssr提升2~3倍,怎么做到的呢?




编译模板的静态标记

我们来看一段很常见的代码

<divid="app">
<h1>hello world</h1>
<p>今天天气真不错</p>
<div>{{name}}</div>
</div>


vue2中会解析

function render(){
with(this){
return _c('div',{
attrs:{
"id":"app"
}
},[_c('h1',[_v("hello world")]), _c('p',[_v("今天天气真不错")]), _c('div',[_v(
_s(name))])])
}
}


其中前面两个标签是完全静态的,后续的渲染中不会产生任何变化, Vue2中依然使用 _c新建成 vdom,在 diff的时候需要对比,有一些额外的性能损耗

我们看下vue3中的解析结果

import{ createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"
exportfunction render(_ctx, _cache){
return(_openBlock(), _createBlock("div",{ id:"app"},[
_createVNode("h1",null,"hello world"),
_createVNode("p",null,"今天天气真不错"),
_createVNode("div",null, _toDisplayString(_ctx.name),1/* TEXT */)
]))
}
// Check the console for the AST

最后一个 _createVNode第四个参数1,只有带这个参数的,才会被真正的追踪,静态节点不需要遍历,这个就是vue3优秀性能的主要来源,再看复杂一点的

<divid="app">
<h1>摸金校尉</h1>
<p>今天天气真不错</p>
<div>{{name}}</div>
<div :class="{red:isRed}">摸金符</div>
<button @click="handleClick">戳我</button>
<inputtype="text"v-model="name">
</div>


解析的结果 在线预览

import{ createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"
exportfunction render(_ctx, _cache){
return(_openBlock(), _createBlock("div",{ id:"app"},[
_createVNode("h1",null,"摸金校尉"),
_createVNode("p",null,"今天天气真不错"),
_createVNode("div",null, _toDisplayString(_ctx.name),1/* TEXT */),
_createVNode("div",{
class:{red:_ctx.isRed}
},"摸金符",2/* CLASS */),
_createVNode("button",{ onClick: _ctx.handleClick },"戳我",8/* PROPS */,["onClick"])
]))
}
// Check the console for the AST


_createVNode出第四个参数出现了别的数字,根据后面注释也很容易猜出,根据 text, props等不同的标记,这样再diff的时候,只需要对比 text或者 props,不用再做无畏的 props遍历, 优秀! 

exportconstenumPatchFlags{
TEXT =1,// 表示具有动态textContent的元素
CLASS =1<<1,// 表示有动态Class的元素
STYLE =1<<2,// 表示动态样式(静态如style="color: red",也会提升至动态)
PROPS =1<<3,// 表示具有非类/样式动态道具的元素。
FULL_PROPS =1<<4,// 表示带有动态键的道具的元素,与上面三种相斥
HYDRATE_EVENTS =1<<5,// 表示带有事件监听器的元素
STABLE_FRAGMENT =1<<6,// 表示其子顺序不变的片段(没懂)。
KEYED_FRAGMENT =1<<7,// 表示带有键控或部分键控子元素的片段。
UNKEYED_FRAGMENT =1<<8,// 表示带有无key绑定的片段
NEED_PATCH =1<<9,// 表示只需要非属性补丁的元素,例如ref或hooks
DYNAMIC_SLOTS =1<<10,// 表示具有动态插槽的元素
}


如果同时有 props和 text的绑定呢, 位运算组合即可

<divid="app">
<h1>摸金校尉</h1>
<p>今天天气真不错</p>
<div :id="userid"">{{name}}</div>
</div>


import{ createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"
exportfunction render(_ctx, _cache){
return(_openBlock(), _createBlock("div",{ id:"app"},[
_createVNode("h1",null,"摸金校尉"),
_createVNode("p",null,"今天天气真不错"),
_createVNode("div",{
id: _ctx.userid,
"\"":""
}, _toDisplayString(_ctx.name),9/* TEXT, PROPS */,["id"])
]))
}
// Check the console for the AST


text是1, props是8,组合在一起就是9,我们可以简单的通过位运算来判定需要做 text和 props的判断, 按位与即可,只要不是0就是需要比较

位运算来做类型组合 本身就是一个最佳实践,react大兄弟也是一样的 代码

exportconst PLUGIN_EVENT_SYSTEM =1;
exportconst RESPONDER_EVENT_SYSTEM =1<<1;
exportconst USE_EVENT_SYSTEM =1<<2;
exportconst IS_TARGET_PHASE_ONLY =1<<3;
exportconst IS_PASSIVE =1<<4;
exportconst PASSIVE_NOT_SUPPORTED =1<<5;
exportconst IS_REPLAYED =1<<6;
exportconst IS_FIRST_ANCESTOR =1<<7;
exportconst LEGACY_FB_SUPPORT =1<<8;

 
事件缓存

绑定的 @click会存在缓存里 链接

<divid="app">
<button @click="handleClick">戳我</button>
</div>


exportfunction render(_ctx, _cache){
return(_openBlock(), _createBlock("div",{ id:"app"},[
_createVNode("button",{
onClick: _cache[1]||(_cache[1]= $event =>(_ctx.handleClick($event)))
},"戳我")
]))
}


传入的事件会自动生成并缓存一个内联函数再cache里,变为一个静态节点。这样就算我们自己写内联函数,也不会导致多余的重复渲染 真是优秀啊

静态提升

代码

<divid="app">
<h1>摸金校尉</h1>
<p>今天天气真不错</p>
<div>{{name}}</div>
<div :class="{red:isRed}">摸金符</div>
</div>


const _hoisted_1 ={ id:"app"}
const _hoisted_2 = _createVNode("h1",null,"摸金校尉",-1/* HOISTED */)
const _hoisted_3 = _createVNode("p",null,"今天天气真不错",-1/* HOISTED */)
exportfunction render(_ctx, _cache){
return(_openBlock(), _createBlock("div", _hoisted_1,[
_hoisted_2,
_hoisted_3,
_createVNode("div",null, _toDisplayString(_ctx.name),1/* TEXT */),
_createVNode("div",{
class:{red:_ctx.isRed}
},"摸金符",2/* CLASS */)
]))
}

vue3和react的vdom

很多人吐槽越来越像 React,其实越来越像的 api,代表着前端的两个方向

Vue1.x

没有 vdom,完全的响应式,每个数据变化,都通过响应式通知机制来新建 Watcher干活,就像独立团规模小的时候,每个战士入伍和升职,都主动通知咱老李,管理方便

项目规模变大后,过多的 Watcher,会导致性能的瓶颈
 

React 15.x

而 React15时代,没有响应式,数据变了,整个新数据和老的数据做 diff,算出差异就知道怎么去修改 dom了,就像老李指挥室有一个模型,每次人事变更,通过对比所有人前后差异,就知道了变化, 看起来有很多计算量,但是这种 immutable的数据结构对大型项目比较友好,而且 Vdom抽象成功后,换成别的平台render成为了可能,无论是打鬼子还是打国军,都用一个 vdom模式。

碰到的问题一样,如果 dom节点持续变多,每次 diff的时间超过了 16ms,就可能会造成卡顿(60fps)。

Vue2.x

引入vdom,控制了颗粒度,组件层面走watcher通知, 组件内部走vdom做diff,既不会有太多watcher,也不会让vdom的规模过大,diff超过16ms,真是优秀啊 就像独立团大了以后,只有营长排长级别的变动,才会通知老李,内部的自己diff管理了。

React 16.x

React走了另外一条路,既然主要问题是 diff导致卡顿,于是 React走了类似 cpu调度的逻辑,把 vdom这棵树,微观变成了链表,利用浏览器的空闲时间来做 diff,如果超过了 16ms,有动画或者用户交互的任务,就把主进程控制权还给浏览器,等空闲了继续,特别像等待女神的备胎。

diff的逻辑,变成了单向的链表,任何时候主线程女神有空了,我们在继续蹭上去接盘做 diff,大家研究下 requestIdleCallback就知道,从浏览器角度看是这样的。

大概代码

requestIdelCallback(myNonEssentialWork);
// 等待女神空闲
function myNonEssentialWork (deadline){
// deadline.timeRemaining()>0 主线程女神还有事件
// 还有diff任务没算玩
while(deadline.timeRemaining()>0&& tasks.length >0){
doWorkIfNeeded();
}
// 女神没时间了,把女神还回去
if(tasks.length >0){
requestIdleCallback(myNonEssentialWork);
}
}

Vue3

这里的静态提升和事件缓存刚才说过了,就不说了,其实我也很纳闷,这些静态标记和事件缓存, React本身也可以做,为啥就不实现了,连 shouldComponentUpdate都得自己定义,为啥不把默认的组件都变成 pure或者 memo呢,唉,也许这就是人生把。

React给你自由, Vue让你持久,可能也是现在国内Vue和React都如此受欢迎的原因吧。

Vue3通过Proxy响应式+组件内部 vdom+静态标记,把任务颗粒度控制的足够细致,所以也不太需要 time-slice了。

Vue3优化Slot性能提升篇

Vue2.x中,如果有一个组件传入了slot,那么每次父组件更新的时候,必定会强制使子组件update,造成性能的浪费。这是由于2.x中,组件的插槽会被当成组件的一个普通children,因此在2.x里面的处理就是只要一个component中传入了slot,那么如果父组件更新,必定会update子组件。
Vue3优化了Slot的生成,使得非动态slot中属性的更新只会触发子组件的更新。动态slot指的是在slot上面使用v-if,v-for,动态slot名字等会导致slot产生运行时动态变化但是又无法被子组件track的操作。(一般来说还是很少在slot上做这种操作的)

Vue3实现这个优化的逻辑主要是这样的:
 
1、首先还是静态编译时候做的工作,给一个Component打上一个PatchFlag标记---是否是DynamicSlot,这一块的逻辑在compiler-core/src/transforms/transformElement.ts中。
2、遇到有传入slot的组件,它的Children不是普通的vnode数组,而是一个slot function的映射表,这些slot function用于在组件中懒生成slot中的vnodes,如下是一个有传入slot的组件生成的render function。


_createVNode(_component_sub_com, null, {
      // _withCtx使得可以访问父组件的context
      default: _withCtx(() => [
        _createTextVNode(_toDisplayString(_ctx.count), 1 /* TEXT */)
      ]),
      _: 1
    })

3、在子组件的render函数里面,调用相应的slot生成函数,因此这个slot函数里面的属性都会被当前的组件实例所track。

4、以上过程就实现了插槽被正确的组件实例所追踪,最后,关于第一步所打的标记,如果传入的slot是动态slot,那么会在第二步的createVNode函数中传入DynamicSlot的PatchFlag,在虚拟dom的patch过程中,遇到一个组件有DynamicSlot,就和Vue 2.x 一样,随着父组件更新强制更新这个组件。

其实和优化虚拟DOM的原理差不多哦。
精品好课
最新完整VUE+ES6视频教程从入门到精通,企业级实战项目
VUE是目前最火的前端框架之一,就业薪资很高,本课程教您如何快速学会VUE+ES6并应用到实战,教你如何解决内存泄漏,常用UI库的使用,自己封装组件,正式上线白屏问题,性能优化等。对正在工作当中或打算学习VUE高薪就...
HTML5基础入门视频教程易学必会
HTML5基础入门视频教程,教学思路清晰,简单易学必会。适合人群:创业者,只要会打字,对互联网编程感兴趣都可以学。课程概述:该课程主要讲解HTML(学习HTML5的必备基础语言)、CSS3、Javascript(学习...
HTML5视频播放器video开发教程
适用人群1、有html基础2、有css基础3、有javascript基础课程概述手把手教你如何开发属于自己的HTML5视频播放器,利用mp4转成m3u8格式的视频,并在移动端和PC端进行播放支持m3u8直播格式,兼容...
jQuery视频教程从入门到精通
jquery视频教程从入门到精通,课程主要包含:jquery选择器、jquery事件、jquery文档操作、动画、Ajax、jquery插件的制作、jquery下拉无限加载插件的制作等等......
最新完整React视频教程从入门到精通纯干货纯实战
React是目前最火的前端框架,就业薪资很高,本课程教您如何快速学会React并应用到实战,教你如何解决内存泄漏,常用UI库的使用,自己封装组件,正式上线白屏问题,性能优化等。对正在工作当中或打算学习React高薪就...
最新完整React+VUE视频教程从入门到精,企业级实战项目
React和VUE是目前最火的前端框架,就业薪资很高,本课程教您如何快速学会React和VUE并应用到实战,教你如何解决内存泄漏,常用库的使用,自己封装组件,正式上线白屏问题,性能优化等。对正在工作当中或打算学习Re...
React实战视频教程仿京东移动端电商
React是前端最火的框架之一,就业薪资很高,本课程教您如何快速学会React并应用到实战,对正在工作当中或打算学习React高薪就业的你来说,那么这门课程便是你手中的葵花宝典。
收藏
扫一扫关注我们