• home > webfront > ECMAS > vue3 >

    vue2升级vue3:vue3比vue2究竟好在哪里?

    Author:zhoulujun Date:

    vue2升级vue3 已经做了一段时间了,官方说性能提升2-3呗。实际上并没有那么夸张。More efficient ref implementation (~260% faster

    vue2升级vue3 已经做了一段时间了,官方说性能提升2-3呗。实际上并没有那么夸张。

    More efficient ref implementation (~260% faster read / ~50% faster write)

    ~40% faster dependency tracking

    ~17% less memory usage

    vue2升级vue3,不想react 丝滑升级。代价还是不少。如果只是单纯提升性能,目前代价还是太大。

    如果是新项目,直接用vue3做更好。

    编译时对VDom的性能优化

    对vue有点基础的应该都知道vue生成dom的方式是通过 ’虚拟dom‘ 渲染 。下面我通过代码简单来阐述下虚拟dom是什么?

    <div class='xxhh'>哈哈<div>
    <div>{{msg}}</div>

    上面是很简单的两个html,vue会将这段dom转换成js代码,“可能”如下所示:

    [{
      html:'div',
      text:'哈哈',
      class:'xxhh',
      children:null
    },
    {
      html:'div',
      text:'$msg',
      children:null
    }]

    生成的这段跟dom结构一样的树形js代码就是虚拟dom了。

    把虚拟dom树交给一个render()函数处理一下就能生成我们肉眼可见的html了。

    那Diff()算法是用来干嘛的呐?

    简单点说:diff算法用于比较js树的更新,当执行一次更新模板的操作后,diff算法用于比较更新的内容是什么,并告诉render()函数哪里需要更新。比如当msg变量更新后,diff算法就会通过全量比较得出一个结论,msg变了,所以跟msg相关的dom节点都需要更新。diff算法的作用我们已经知道了,用代码实现如下:

    function diff(oldDom,newDom){
     // oldDom 和 newDom 一一比较
     return needUpdateDom
    }

    谷歌一下,资料不要太多: https://www.google.com/search?q=vue3+diff+源码&oq=vue3++diff+源码



    diff算法优化

    了解过Vue2的朋友们肯定知道diff算法,所谓diff算法是Vue将模板编译成渲染函数生成虚拟DOM树,通过递归遍历两个虚拟DOM树,并比较每个节点上的每个属性,来确定实际DOM哪些部分需要更新。虽然经过现代JS引擎执行的高级优化之后,这种算法非常快速,但是在实际使用中DOM的更新仍然设计到许多不必要的工作(具体表现在静态内容,只有少量动态模板的情况)。

    比如下面这段代码

    <div>
        <p>老八食堂</p>
        <p>{{ message }}</p>
    </div>

    vue2的diff全量对比:


    vue2 diff 更新

    在之前的VDOM中,如果msg值发生改变,整个模版中的所有元素都需要重新渲染

    而在Vue3中使用patchFlag(静态标记)对diff算法进行了优化,在创建虚拟DOM时,根据DOM内容是否会发生变化,而给与相应类型的patchFlag在更新前节点进行比较的时候,只会去对比带有静态标记的节点

    vue3 patchFlag 优化


    vue3 diff更新

    通过上图,Vue3的diff算法只对带有静态标记的DOM进行比较,只需要对比一次,而Vue2中的diff算法却比较了3次。这便是Vue3比Vue2性能好的一个原因。 

    接下来我们通过模板转化网站 把模板转化成虚拟DOM来验证一些上述分析:

    import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
    
    export function render(_ctx, _cache, $props, $setup, $data, $options) {
    return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("p", null, "老八食堂"),
    _createElementVNode("p", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
    ]))
    }

    message对应的P标签有个注释为TEXT的1,这个1就是patchFlag。

    Vue3.0在模版编译时,编译器会在动态标签末尾加上 /* Text*/ PatchFlag。只能带patchFlag 的 Node 才被认为是动态的元素,会被追踪属性的修改。并且 PatchFlag 会标识动态的属性类型有哪些,比如这里 的TEXT 表示只有节点中的文字是动态的。

    每一个Block中的节点,就算很深,也是直接跟Block一层绑定的,可以直接跳转到动态节点而不需要逐个逐层遍历。

    既有VDOM的灵活性,又有性能保证。

    而Vue3对不同类型的元素会有不同的flag标记

    再来看一段很常见的代码

    <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

    vue3 diff优化

    给更多查看:vue2、vue3区别虚拟dom和Slot性能提升的原理 www.lucklnk.com/godaddy/details/aid/179291165

    再diff的时候,只需要对比 text或者 props,不用再做无畏的 props遍历

    这里安利一些:


    Vue3源码中的Flag类型

    export const enum PatchFlags { 
        TEXT = 1,// 动态的文本节点 
        CLASS = 1 << 1, // 2 动态的 class 
        STYLE = 1 << 2, // 4 动态的 style 
        PROPS = 1 << 3, // 8 动态属性,不包括类名和样式 
        FULL_PROPS = 1 << 4, // 16 动态 key,当 key 变化时需要完整的 diff 算法做比较 
        HYDRATE_EVENTS = 1 << 5, // 32 表示带有事件监听器的节点 
        STABLE_FRAGMENT = 1 << 6, // 64 一个不会改变子节点顺序的 
        Fragment KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性的 Fragment 
        UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 Fragment 
        NEED_PATCH = 1 << 9, // 512 
        DYNAMIC_SLOTS = 1 << 10, // 动态 solt 
        HOISTED = -1, // 特殊标志是负整数表示永远不会用作 diff 
        BAIL = -2 // 一个特殊的标志,指代差异算法 
    }

    如果同时有 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就是需要比较

    vue diff 位运算

    位运算来做类型组合 本身就是一个最佳实践,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;


    hoistStatic 静态提升

    我们在平时开发中,会将一些常量提升出去定义,如下所示:

    const PAGE_SIZE = 20
    function getDate() {
        $.get({
            url: '',
            data: {
                pageSize: PAGE_SIZE
            }
        })
    }

    如上面的例子,如果我们将PAGE_SIZE写在getData内,每次调用都会重新定义一次PAGE_SIZE变量

    在Vue3中也做了类似上面的优化,Vue3的编译器会将模板中的静态节点、子树、数据对象,并在生成的代码中将他们提升到渲染函数之外,一次来避免每次调用渲染函数的时候重新创建这些对象,从而大大提高内存使用率并减少垃圾回收频率

    当使用hoistStatic时,所有 静态的节点都被提升到render方法之外。这意味着,他们只会在应用启动的时候被创建一次,而后随着每次的渲染被不停的复用

    在大型应用中对于内存有很大的优化。

    vue3提升变量示意图

    上图中红框圈起来的地方,将模板中的静态模板提升到了render函数外,这又提升了Vue的性能。


    cacheHandlers 事件监听缓存

    正常情况下,当绑定一个事件:

    <div>
      <p @click="handleClick">静态代码</p>
    </div>

    模版会被编译为

    export function render(_ctx, _cache) {
      return (_openBlock(), _createBlock("div", null, [
        _createVNode("p", { onClick: _ctx.handleClick }, "静态代码", 8 /* PROPS */, ["onClick"])
      ]))
    }

    其中事件会每次从全局上下文中获取。而当开启了cacheHandler之后

    export function render(_ctx, _cache) {
      return (_openBlock(), _createBlock("div", null, [
        _createVNode("p", {
          onClick: _cache[1] || (_cache[1] = ($event, ...args) => (_ctx.handleClick($event, ...args)))
        }, "静态代码")
      ]))
    }

    编辑器会为你动态创建一个内联函数,内联函数里面再去引用当前组件上最新的handler。之后编辑器会将内联函数缓存。每次重新渲染时如果事件处理器没有变,就会使用缓存中的事件处理而不会重新获取事件处理器。这个节点就可以被看作是一个静态的节点

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

    这种优化更大的作用在于当其作用域组件时,之前每次重新渲染都会导致组件的重新渲染,在通过handler缓存之后,不会导致组件的重新渲染了


    StaticNode 静态节点

    当静态节点嵌套足够多的时候,VUE编译器也会将VDOM转化为纯字符串的HTML。即 StaticNode。

    2.jpeg

    每个元素都会变成虚拟节点,一大堆的虚拟节点

    转载本站文章《vue2升级vue3:vue3比vue2究竟好在哪里?》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/vue3/8863.html