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 過程如圖所示:
可以看到,因為這段程式碼中只有一個動態節點,所以這裡有很多diff 和遍歷其實都是不需要的,這就會導致vnode 的效能跟模版大小正相關,跟動態節點的數量無關,當一些元件的整個模版內只有少量動態節點時,這些遍歷都是效能的浪費。對於上述例子,理想狀態只需要 diff 這個綁定 message 動態節點的 p 標籤。
Vue.js 3.0
透過編譯階段對靜態模板的分析,編譯產生了 Block tree
。
Block tree
是一個將模板基於動態節點指令切割的嵌套區塊,每個區塊內部的節點結構是固定的,而且每個區塊只需要以一個Array
來追蹤自身包含的動態節點。借助 Block tree
,Vue.js 將 vnode 更新效能由與模版整體大小相關提升為與動態內容的數量相關,這是一個非常大的效能突破。
由於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算法应该结束 }
左邊的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 如下:
#此時產生的虛擬節點多出一個dynamicChildren
屬性,裡面收集了動態節點span
。
我們之前分析過,在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]) } }
接下來我們來看看屬性比對的最佳化策略:
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
會充分利用patchFlag
和dynamicChildren
做最佳化。如果確定只是某個局部的變動,例如style
改變,那麼只會呼叫hostPatchProp
並傳入對應的參數style
# 做特定的更新(靶向更新);如果有dynamicChildren
,會執行patchBlockChildren
做對比更新,不會每次都對props 和子節點進行全量的對比更新。圖解如下:
静态提升是将静态的节点或者属性提升出去,假设有以下模板:
<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
在编译阶段做的优化,基于上面几点,Vuejs
在 patch
过程中极大地提高了性能。
以上是vue3編譯做了哪些最佳化的詳細內容。更多資訊請關注PHP中文網其他相關文章!