首頁 > web前端 > Vue.js > 主體

vue3編譯最佳化的內容有哪些

王林
發布: 2023-05-17 17:50:16
轉載
1262 人瀏覽過

vue3編譯優化有:1、引入了patchFlag,用來標記動態內容;在編譯過程中會根據不同的屬性類型打上不同的標識,從而實現了快速diff演算法。 2、Block Tree。 3.靜態提升,是將靜態的節點或屬性提升出去。當靜態節點連續超過10個時,預處理將進行字串化並合併為連續的靜態節點序列。啟用cacheHandlers選項後,函數會被緩存,以便於之後直接呼叫。

本文主要來分析 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 過程如圖所示:

vue3編譯最佳化的內容有哪些

可以看到,因為這段程式碼中只有一個動態節點,所以這裡有很多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

vue3編譯最佳化的內容有哪些

左邊的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 如下:

vue3編譯最佳化的內容有哪些

#此時產生的虛擬節點多出一個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, &#39;class&#39;, null, newProps.class, ...)
        }
      }
      // 存在动态 style 属性时
      if (patchFlag & PatchFlags.STYLE) {
        hostPatchProp(el, &#39;style&#39;, 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 === &#39;class&#39;) { // 更新 class 
    patchClass(el, nextValue)
  } else if (key === &#39;style&#39;) { // 更新 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(&#39;class&#39;); // 如果不需要class直接移除
  } else {
    el.className = nextValue
  }
}

function patchStyle(el, prevValue, nextValue = {}){
  ...
}

function patchAttr(el, key, nextValue){
  ...
}
登入後複製

總結: vue3 會充分利用patchFlagdynamicChildren 做最佳化。如果確定只是某個局部的變動,例如style 改變,那麼只會呼叫hostPatchProp 並傳入對應的參數style# 做特定的更新(靶向更新);如果有dynamicChildren,會執行patchBlockChildren 做對比更新,不會每次都對props 和子節點進行全量的對比更新。圖解如下:

vue3編譯最佳化的內容有哪些

靜態提升

#靜態提升是將靜態的節點或屬性提升出去,假設有以下模板:

<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)
  }))
}
登入後複製

以上是vue3編譯最佳化的內容有哪些的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:yisu.com
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!