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編譯做了哪些優化的詳細內容,更多請關注TW511.COM其它相關文章!