vue2、vue3区别虚拟dom和Slot性能提升的原理
日期:2020-08-14
来源:程序思维浏览:3163次
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;
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的原理差不多哦。
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,会导致性能的瓶颈
绑定的 @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实现这个优化的逻辑主要是这样的:
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.use源码解析
- 下一篇:手写vue源码之vue-router路由附源码下载
精品好课