• 技术文章 >web前端 >前端问答

    vue3编译做了哪些优化

    青灯夜游青灯夜游2022-12-19 18:05:58原创115

    vue3编译优化有:1、引入了 patchFlag,用来标记动态内容;在编译过程中会根据不同的属性类型打上不同的标识,从而实现了快速diff算法。2、Block Tree。3、静态提升,是将静态的节点或者属性提升出去。4、预解析字符串化,当连续静态节点超过10个时,会将静态节点序列化为字符串。5、函数缓存;开启cacheHandlers选项后,函数会被缓存起来,后续可直接使用。

    本教程操作环境:windows7系统、vue3版,DELL G3电脑。

    本文主要来分析 Vue3.0 编译阶段做的优化,在 patch 阶段是如何利用这些优化策略来减少比对次数。 由于组件更新时依然需要遍历该组件的整个 vnode 树,比如下面这个模板:

    <template>
      <div id="container">
        <p class="text">static text</p>
        <p class="text">static text</p>
        <p class="text">{{ message }}</p>
        <p class="text">static text</p>
        <p class="text">static text</p>
      </div>
    </template>

    整个 diff 过程如图所示:

    1.png

    可以看到,因为这段代码中只有一个动态节点,所以这里有很多 diff 和遍历其实都是不需要的,这就会导致 vnode 的性能跟模版大小正相关,跟动态节点的数量无关,当一些组件的整个模版内只有少量动态节点时,这些遍历都是性能的浪费。对于上述例子,理想状态只需要 diff 这个绑定 message 动态节点的 p 标签即可。

    Vue.js 3.0 通过编译阶段对静态模板的分析,编译生成了 Block tree

    Block tree 是一个将模板基于动态节点指令切割的嵌套区块,每个区块内部的节点结构是固定的,而且每个区块只需要以一个 Array 来追踪自身包含的动态节点。借助 Block treeVue.js 将 vnode 更新性能由与模版整体大小相关提升为与动态内容的数量相关,这是一个非常大的性能突破。

    PatchFlag

    由于 diff 算法无法避免新旧虚拟 DOM 中无用的比较操作,Vue.js 3.0 引入了 patchFlag,用来标记动态内容。在编译过程中会根据不同的属性类型打上不同的标识,从而实现了快速 diff 算法。PatchFlags 的所有枚举类型如下所示:

    export const enum PatchFlags {
      TEXT = 1, // 动态文本节点
      CLASS = 1 << 1, // 动态class
      STYLE = 1 << 2, // 动态style
      PROPS = 1 << 3, // 除了class、style动态属性
      FULL_PROPS = 1 << 4, // 有key,需要完整diff
      HYDRATE_EVENTS = 1 << 5, // 挂载过事件的
      STABLE_FRAGMENT = 1 << 6, // 稳定序列,子节点顺序不会发生变化
      KEYED_FRAGMENT = 1 << 7, // 子节点有key的fragment
      UNKEYED_FRAGMENT = 1 << 8, // 子节点没有key的fragment
      NEED_PATCH = 1 << 9, // 进行非props比较, ref比较
      DYNAMIC_SLOTS = 1 << 10, // 动态插槽
      DEV_ROOT_FRAGMENT = 1 << 11, 
      HOISTED = -1, // 表示静态节点,内容变化,不比较儿子
      BAIL = -2 // 表示diff算法应该结束
    }

    Block Tree

    2.png

    左侧的 template 经过编译后会生成右侧的 render 函数,里面有 _openBlock_createElementBlock_toDisplayString_createElementVNode(createVnode) 等辅助函数。

    let currentBlock = null
    function _openBlock() {
      currentBlock = [] // 用一个数组来收集多个动态节点
    }
    function _createElementBlock(type, props, children, patchFlag) {
      return setupBlock(createVnode(type, props, children, patchFlag));
    }
    
    export function createVnode(type, props, children = null, patchFlag = 0) {
      const vnode = {
        type,
        props,
        children,
        el: null, // 虚拟节点上对应的真实节点,后续diff算法
        key: props?.["key"],
        __v_isVnode: true,
        shapeFlag,
        patchFlag 
      };
      ...
    
      if (currentBlock && vnode.patchFlag > 0) {
        currentBlock.push(vnode);
      }
      return vnode;
    }
    
    function setupBlock(vnode) {
      vnode.dynamicChildren = currentBlock;
      currentBlock = null;
      return vnode;
    }
    
    function _toDisplayString(val) {
      return isString(val)
        ? val
        : val == null
        ? ""
        : isObject(val)
        ? JSON.stringify(val)
        : String(val);
    }

    此时生成的 vnode 如下:

    3.png

    此时生成的虚拟节点多出一个 dynamicChildren 属性,里面收集了动态节点 span

    节点 diff 优化策略:

    我们之前分析过,在 patch 阶段更新节点元素的时候,会执行 patchElement 函数,我们再来回顾一下它的实现:

    const patchElement = (n1, n2) => { // 先复用节点、在比较属性、在比较儿子
      let el = n2.el = n1.el;
      let oldProps = n1.props || {}; // 对象
      let newProps = n2.props || {}; // 对象
      patchProps(oldProps, newProps, el);
    
      if (n2.dynamicChildren) { // 只比较动态元素
        patchBlockChildren(n1, n2);
      } else {
        patchChildren(n1, n2, el); // 全量 diff
      }
    }

    我们在前面组件更新的章节分析过这个流程,在分析子节点更新的部分,当时并没有考虑到优化的场景,所以只分析了全量比对更新的场景。

    而实际上,如果这个 vnode 是一个 Block vnode,那么我们不用去通过 patchChildren 全量比对,只需要通过 patchBlockChildren 去比对并更新 Block 中的动态子节点即可。 由此可以看出性能被大幅度提升,从 tree 级别的比对,变成了线性结构比对。

    我们来看一下它的实现:

    const patchBlockChildren = (n1, n2) => {
      for (let i = 0; i < n2.dynamicChildren.length; i++) {
        patchElement(n1.dynamicChildren[i], n2.dynamicChildren[i])
      }
    }

    属性 diff 优化策略:

    接下来我们看一下属性比对的优化策略:

    const patchElement = (n1, n2) => { // 先复用节点、在比较属性、在比较儿子
      let el = n2.el = n1.el;
      let oldProps = n1.props || {}; // 对象
      let newProps = n2.props || {}; // 对象
      let { patchFlag, dynamicChildren } = n2
      
      if (patchFlag > 0) {
        if (patchFlag & PatchFlags.FULL_PROPS) { // 对所 props 都进行比较更新
          patchProps(el, n2, oldProps, newProps, ...)
        } else {
          // 存在动态 class 属性时
          if (patchFlag & PatchFlags.CLASS) {
            if (oldProps.class !== newProps.class) {
              hostPatchProp(el, 'class', null, newProps.class, ...)
            }
          }
          // 存在动态 style 属性时
          if (patchFlag & PatchFlags.STYLE) {
            hostPatchProp(el, 'style', oldProps.style, newProps.style, ...)
          }
          
          // 针对除了 style、class 的 props
          if (patchFlag & PatchFlags.PROPS) {
            const propsToUpdate = n2.dynamicProps!
            for (let i = 0; i < propsToUpdate.length; i++) {
              const key = propsToUpdate[i]
              const prev = oldProps[key]
              const next = newProps[key]
              if (next !== prev) {
                hostPatchProp(el, key, prev, next, ...)
              }
            }
          }
          if (patchFlag & PatchFlags.TEXT) { // 存在动态文本
            if (n1.children !== n2.children) {
              hostSetElementText(el, n2.children as string)
            }
          } 
        } else if (dynamicChildren == null) {
          patchProps(el, n2, oldProps, newProps, ...)
        }
      }
    }
    
    function hostPatchProp(el, key, prevValue, nextValue) {
      if (key === 'class') { // 更新 class 
        patchClass(el, nextValue)
      } else if (key === 'style') { // 更新 style
        patchStyle(el, prevValue, nextValue)
      } else if (/^on[^a-z]/.test(key)) {  // events  addEventListener
        patchEvent(el, key, nextValue);
      } else { // 普通属性 el.setAttribute
        patchAttr(el, key, nextValue);
      }
    }
    
    function patchClass(el, nextValue) {
      if (nextValue == null) {
        el.removeAttribute('class'); // 如果不需要class直接移除
      } else {
        el.className = nextValue
      }
    }
    
    function patchStyle(el, prevValue, nextValue = {}){
      ...
    }
    
    function patchAttr(el, key, nextValue){
      ...
    }

    总结: vue3 会充分利用 patchFlagdynamicChildren 做优化。如果确定只是某个局部的变动,比如 style 改变,那么只会调用 hostPatchProp 并传入对应的参数 style 做特定的更新(靶向更新);如果有 dynamicChildren,会执行 patchBlockChildren 做对比更新,不会每次都对 props 和子节点进行全量的对比更新。图解如下:

    4.png

    静态提升

    静态提升是将静态的节点或者属性提升出去,假设有以下模板:

    <div>
      <span>hello</span> 
      <span a=1 b=2>{{name}}</span>
      <a><span>{{age}}</span></a>
    </div>

    编译生成的 render 函数如下:

    export function render(_ctx, _cache, $props, $setup, $data, $options) {
      return (_openBlock(), _createElementBlock("div", null, [
        _createElementVNode("span", null, "hello"),
        _createElementVNode("span", {
          a: "1",
          b: "2"
        }, _toDisplayString(_ctx.name), 1 /* TEXT */),
        _createElementVNode("a", null, [
          _createElementVNode("span", null, _toDisplayString(_ctx.age), 1 /* TEXT */)
        ])
      ]))
    }

    我们把模板编译成 render 函数是这个酱紫的,那么问题就是每次调用 render 函数都要重新创建虚拟节点。

    开启静态提升 hoistStatic 选项后

    const _hoisted_1 = /*#__PURE__*/_createElementVNode("span", null, "hello", -1 /* HOISTED */)
    const _hoisted_2 = {
      a: "1",
      b: "2"
    }
    
    export function render(_ctx, _cache, $props, $setup, $data, $options) {
      return (_openBlock(), _createElementBlock("div", null, [
        _hoisted_1,
        _createElementVNode("span", _hoisted_2, _toDisplayString(_ctx.name), 1 /* TEXT */),
        _createElementVNode("a", null, [
          _createElementVNode("span", null, _toDisplayString(_ctx.age), 1 /* TEXT */)
        ])
      ]))
    }

    预解析字符串化

    静态提升的节点都是静态的,我们可以将提升出来的节点字符串化。 当连续静态节点超过 10 个时,会将静态节点序列化为字符串。

    假如有如下模板:

    <div>
      <span>static</span>
      <span>static</span>
      <span>static</span>
      <span>static</span>
      <span>static</span>
      <span>static</span>
      <span>static</span>
      <span>static</span>
      <span>static</span>
      <span>static</span>
    </div>

    开启静态提升 hoistStatic 选项后

    const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<span>static</span><span>static</span><span>static</span><span>static</span><span>static</span><span>static</span><span>static</span><span>static</span><span>static</span><span>static</span>", 10)
    const _hoisted_11 = [  _hoisted_1]
    
    export function render(_ctx, _cache, $props, $setup, $data, $options) {
      return (_openBlock(), _createElementBlock("div", null, _hoisted_11))
    }

    函数缓存

    假如有如下模板:

    <div @click="event => v = event.target.value"></div>

    编译后:

    const _hoisted_1 = ["onClick"]
    
    export function render(_ctx, _cache, $props, $setup, $data, $options) {
      return (_openBlock(), _createElementBlock("div", {
        onClick: event => _ctx.v = event.target.value
      }, null, 8 /* PROPS */, _hoisted_1))
    }

    每次调用 render 的时候要创建新函数,开启函数缓存 cacheHandlers 选项后,函数会被缓存起来,后续可以直接使用

    export function render(_ctx, _cache, $props, $setup, $data, $options) {
      return (_openBlock(), _createElementBlock("div", {
        onClick: _cache[0] || (_cache[0] = event => _ctx.v = event.target.value)
      }))
    }

    总结

    以上几点即为 Vuejs 在编译阶段做的优化,基于上面几点,Vuejspatch 过程中极大地提高了性能。

    【相关推荐:vuejs视频教程web前端开发

    以上就是vue3编译做了哪些优化的详细内容,更多请关注php中文网其它相关文章!

    声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn核实处理。
    专题推荐:前端 Vue.js
    上一篇:vue封装axios有什么用 下一篇:自己动手写 PHP MVC 框架(40节精讲/巨细/新人进阶必看)

    相关文章推荐

    • 一文聊聊Vue组件生命周期的三个阶段(创建、运行和销毁)• vue项目只有一个vue实例么• vue适用多页面应用吗• 怎么查询当前vue的版本• vue运用了哪些模式• vue组件中data不能是函数吗
    1/1

    PHP中文网