Vue3 編譯中的優(yōu)化

大佬總結(jié),知乎轉(zhuǎn)載
https://zhuanlan.zhihu.com/p/150732926

image.png

Vue3Compilerruntime 緊密合作,充分利用編譯時(shí)信息,使得性能得到了極大的提升。本文的目的告訴你 Vue3Compiler 到底做了哪些優(yōu)化,以及一些你可能希望知道的優(yōu)化細(xì)節(jié),在這個(gè)基礎(chǔ)上我們試著總結(jié)出一套手寫優(yōu)化模式的高性能渲染函數(shù)的方法,這些知識也可以用于實(shí)現(xiàn)一個(gè) Vue3jsx babel 插件中,讓 jsx 也能享受優(yōu)化模式的運(yùn)行時(shí)收益,這里需要澄清的是,即使在非優(yōu)化模式下,理論上 Vue3Diff 性能也是要優(yōu)于 Vue2 的。另外本文不包括 SSR 相關(guān)優(yōu)化,希望在下篇文章總結(jié)。

篇幅較大,花費(fèi)了很大的精力整理,對于對 Vue3 還沒有太多了解的同學(xué)閱讀起來也許會吃力,不妨先收藏,以后也許會用得到。

按照慣例 TOC:

  • Block Tree 和 PatchFlags

    • 傳統(tǒng) Diff 算法的問題
    • Block 配合 PatchFlags 做到靶向更新
    • 節(jié)點(diǎn)不穩(wěn)定 - Block Tree
    • v-if 的元素作為 Block
    • v-for 的元素作為 Block
    • 不穩(wěn)定的 Fragment
    • 穩(wěn)定的 Fragment
    • v-for 的表達(dá)式是常量
    • 多個(gè)根元素
    • 插槽出口
    • <template v-for>
  • 靜態(tài)提升

    • 提升靜態(tài)節(jié)點(diǎn)樹
    • 元素不會被提升的情況
    • 元素帶有動態(tài)的 key 綁定
    • 使用 ref 的元素
    • 使用自定義指令的元素
    • 提升靜態(tài) PROPS
  • 預(yù)字符串化

  • Cache Event handler

  • v-once

  • 手寫高性能渲染函數(shù)

    • 幾個(gè)需要記住的小點(diǎn)
    • Block Tree 是靈活的
    • 正確地使用 PatchFlags
    • NEED_PATCH
    • 該使用 Block 的地方必須用
    • 分支判斷使用 Block
    • 列表使用 Block
    • 使用動態(tài) key 的元素應(yīng)該是 Block
    • 使用 Slot hint
    • 為組件正確地使用 DYNAMIC_SLOTS
    • 使用 $stable hint

Block Tree 和 PatchFlags

Block TreePatchFlagsVue3 充分利用編譯信息并在 Diff 階段所做的優(yōu)化。尤大已經(jīng)不止一次在公開場合聊過思路,我們深入細(xì)節(jié)的目的是為了更好的理解,并試圖手寫出高性能的 VNode

傳統(tǒng) Diff 算法的問題

“傳統(tǒng) vdom”的 Diff 算法總歸要按照 vdom 樹的層級結(jié)構(gòu)一層一層的遍歷(如果你對各種傳統(tǒng) diff 算法不了解,可以看我之前寫《渲染器》這套文章,里面總結(jié)了三種傳統(tǒng) Diff 方式),舉個(gè)例子如下模板所示:

<div>
    <p class="foo">bar</p>
</div>

對于傳統(tǒng) diff 算法來說,它在 diff 這段 vnode(模板編譯后的 vnode)時(shí)會經(jīng)歷:

  • Div 標(biāo)簽的屬性 + children

  • <p> 標(biāo)簽的屬性(class) + children

  • 文本節(jié)點(diǎn):bar

但是很明顯,這明明就是一段靜態(tài) vdom,它在組件更新階段是不可能發(fā)生變化的。如果能在 diff 階段跳過靜態(tài)內(nèi)容,那就會避免無用的 vdom 樹的遍歷和比對,這應(yīng)該就是最早的優(yōu)化思路來源----跳過靜態(tài)內(nèi)容,只對比動態(tài)內(nèi)容

Block 配合 PatchFlags 做到靶向更新

咱們先說 Block 再聊 Block Tree。現(xiàn)在思路有了,我們只希望對比非靜態(tài)的內(nèi)容,例如:

<div>
    <p>foo</p>
    <p>{{ bar }}</p>
</div>

在這段模板中,只有 <p>{{ bar }}</p> 中的文本節(jié)點(diǎn)是動態(tài)的,因此只需要靶向更新該文本節(jié)點(diǎn)即可,這在包含大量靜態(tài)內(nèi)容而只有少量動態(tài)內(nèi)容的場景下,性能優(yōu)勢尤其明顯。可問題是怎么做呢?我們需要拿到整顆 vdom 樹中動態(tài)節(jié)點(diǎn)的能力,其實(shí)可能沒有大家想像的復(fù)雜,來看下這段模板對應(yīng)的傳統(tǒng) vdom 樹大概長什么樣:

const vnode = {
    tag: 'div',
    children: [
        { tag: 'p', children: 'foo' },
        { tag: 'p', children: ctx.bar },  // 這是動態(tài)節(jié)點(diǎn)
    ]
}

在傳統(tǒng)的 vdom 樹中,我們在運(yùn)行時(shí)得不到任何有用信息,但是 Vue3compiler 能夠分析模板并提取有用信息,最終體現(xiàn)在 vdom 樹上。例如它能夠清楚的知道:哪些節(jié)點(diǎn)是動態(tài)節(jié)點(diǎn),以及為什么它是動態(tài)的(是綁定了動態(tài)的 class?還是綁定了動態(tài)的 style?亦或是其它動態(tài)的屬性?),總之編譯器能夠提取我們想要的信息,有了這些信息我們就可以在創(chuàng)建 vnode 的過程中為動態(tài)的節(jié)點(diǎn)打上標(biāo)記:也就是傳說中的 PatchFlags

我們可以把 PatchFlags 簡單的理解為一個(gè)數(shù)字標(biāo)記,把這些數(shù)字賦予不同含義,例如:

  • 數(shù)字 1:代表節(jié)點(diǎn)有動態(tài)的 textContent(例如上面模板中的 p 標(biāo)簽)
  • 數(shù)字 2:代表元素有動態(tài)的 class 綁定
  • 數(shù)字 3:代表xxxxx

總之我們可以預(yù)設(shè)這些含義,最后體現(xiàn)在 vnode 上:

const vnode = {
    tag: 'div',
    children: [
        { tag: 'p', children: 'foo' },
        { tag: 'p', children: ctx.bar, patchFlag: 1 /* 動態(tài)的 textContent */ },
    ]
}

有了這個(gè)信息,我們就可以在 vnode 的創(chuàng)建階段把動態(tài)節(jié)點(diǎn)提取出來,什么樣的節(jié)點(diǎn)是動態(tài)節(jié)點(diǎn)呢?帶有 patchFlag 的節(jié)點(diǎn)就是動態(tài)節(jié)點(diǎn),我們將它提取出來放到一個(gè)數(shù)組中存著,例如:

const vnode = {
    tag: 'div',
    children: [
        { tag: 'p', children: 'foo' },
        { tag: 'p', children: ctx.bar, patchFlag: 1 /* 動態(tài)的 textContent */ },
    ],
    dynamicChildren: [
        { tag: 'p', children: ctx.bar, patchFlag: 1 /* 動態(tài)的 textContent */ },
    ]
}

dynamicChildren 就是我們用來存儲一個(gè)節(jié)點(diǎn)下所有子代動態(tài)節(jié)點(diǎn)的數(shù)組,注意這里的用詞哦:“子代”,例如:

const vnode = {
    tag: 'div',
    children: [
        { tag: 'section', children: [
            { tag: 'p', children: ctx.bar, patchFlag: 1 /* 動態(tài)的 textContent */ },
        ]},
    ],
    dynamicChildren: [
        { tag: 'p', children: ctx.bar, patchFlag: 1 /* 動態(tài)的 textContent */ },
    ]
}

如上 vnode 所示,div 節(jié)點(diǎn)不僅能收集直接動態(tài)子節(jié)點(diǎn),它還能收集所有子代節(jié)點(diǎn)中的動態(tài)節(jié)點(diǎn)。為什么 div 節(jié)點(diǎn)這么厲害呢?因?yàn)樗鼡碛幸粋€(gè)特殊的角色:Block,沒錯(cuò)這個(gè) div 節(jié)點(diǎn)就是傳說中的 Block一個(gè) Block 其實(shí)就是一個(gè) VNode,只不過它有特殊的屬性(其中之一就是 dynamicChildren)。

現(xiàn)在我們已經(jīng)拿到了所有的動態(tài)節(jié)點(diǎn),它們存儲在 dynamicChildren 中,因此在 diff 過程中就可以避免按照 vdom 樹一層一層的遍歷,而是直接找到 dynamicChildren 進(jìn)行更新。除了跳過無用的層級遍歷之外,由于我們早早的就為 vnode 打上了 patchFlag,因此在更新 dynamicChildren 中的節(jié)點(diǎn)時(shí),可以準(zhǔn)確的知道需要為該節(jié)點(diǎn)應(yīng)用哪些更新動作,這基本上就實(shí)現(xiàn)了靶向更新。

節(jié)點(diǎn)不穩(wěn)定 - Block Tree

一個(gè) Block 怎么也構(gòu)不成 Block Tree,這就意味著在一顆 vdom 樹中,會有多個(gè) vnode 節(jié)點(diǎn)充當(dāng) Block 的角色,進(jìn)而構(gòu)成一顆 Block Tree。那么什么情況下一個(gè) vnode 節(jié)點(diǎn)會充當(dāng) block 的角色呢?

來看下面這段模板:

<div>
  <section v-if="foo">
    <p>{{ a }}</p>
  </section>
  <div v-else>
    <p>{{ a }}</p>
  </div>
</div>

假設(shè)只要最外層的 div 標(biāo)簽是 Block 角色,那么當(dāng) foo 為真時(shí),block 收集到的動態(tài)節(jié)點(diǎn)為:

cosnt block = {
    tag: 'div',
    dynamicChildren: [
        { tag: 'p', children: ctx.a, patchFlag: 1 }
    ]
}

當(dāng) foo 為假時(shí),block 的內(nèi)容如下:

cosnt block = {
    tag: 'div',
    dynamicChildren: [
        { tag: 'p', children: ctx.a, patchFlag: 1 }
    ]
}

可以發(fā)現(xiàn)無論 foo 為真還是假,block 的內(nèi)容是不變的,這就意味什么在 diff 階段不會做任何更新,但是我們也看到了:v-if 的是一個(gè) <section> 標(biāo)簽,v-else 的是一個(gè) <div> 標(biāo)簽,所以這里就出問題了。實(shí)際上問題的本質(zhì)在于 dynamicChildrendiff 是忽略 vdom 樹層級的,如下模板也有同樣的問題:

<div>
  <section v-if="foo">
    <p>{{ a }}</p>
  </section>
  <section v-else> <!-- 即使這里是 section -->
       <div> <!-- 這個(gè) div 標(biāo)簽在 diff 過程中被忽略 -->
            <p>{{ a }}</p>
        </div>
  </section >
</div>

即使 v-else 的也是一個(gè) <section> 標(biāo)簽,但由于前后 DOM 樹的不穩(wěn)定,也會導(dǎo)致問題。這時(shí)我們就思考,如何讓 DOM 樹的結(jié)構(gòu)變穩(wěn)定呢?

v-if 的元素作為 Block

如果讓使用了 v-if/v-else-if/v-else 等指令的元素也作為 Block 會怎么樣呢?我們拿如下模板為例:

<div>
  <section v-if="foo">
    <p>{{ a }}</p>
  </section>
  <section v-else> <!-- 即使這里是 section -->
       <div> <!-- 這個(gè) div 標(biāo)簽在 diff 過程中被忽略 -->
            <p>{{ a }}</p>
        </div>
  </section >
</div>

如果我們讓這兩個(gè) section 標(biāo)簽都作為 block,那么將構(gòu)成一顆 block tree

Block(Div)
    - Block(Section v-if)
    - Block(Section v-else)

父級 Block 除了會收集子代動態(tài)節(jié)點(diǎn)之外,也會收集子 Block,因此兩個(gè) Block(section) 將作為 Block(div)dynamicChildren

cosnt block = {
    tag: 'div',
    dynamicChildren: [
        { tag: 'section', { key: 0 }, dynamicChildren: [...]}, /* Block(Section v-if) */
        { tag: 'section', { key: 1 }, dynamicChildren: [...]}  /* Block(Section v-else) */
    ]
}

這樣當(dāng) v-if 條件為真時(shí),dynamicChildren 中包含的是 Block(section v-if),當(dāng)條件為假時(shí) dynamicChildren 中包含的是 Block(section v-else),在 Diff 過程中,渲染器知道這是兩個(gè)不同的 Block,因此會做完全的替換,這樣就解決了 DOM 結(jié)構(gòu)不穩(wěn)定引起的問題。而這就是 Block Tree

v-for 的元素作為 Block

不僅 v-if 會讓 DOM 結(jié)構(gòu)不穩(wěn)定,v-for 也會,但是 v-for 的情況稍微復(fù)雜一些。思考如下模板:

<div>
    <p v-for="item in list">{{ item }}</p>
    <i>{{ foo }}</i>
    <i>{{ bar }}</i>
</div>

假設(shè) list 值由 [1 ,2] 變?yōu)?[1],按照之前的思路,最外層的 <div> 標(biāo)簽作為一個(gè) Block,那么它更新前后對應(yīng)的 Block Tree 應(yīng)該是:

// 前
const prevBlock = {
    tag: 'div',
    dynamicChildren: [
        { tag: 'p', children: 1, 1 /* TEXT */ },
        { tag: 'p', children: 2, 1 /* TEXT */ },
        { tag: 'i', children: ctx.foo, 1 /* TEXT */ },
        { tag: 'i', children: ctx.bar, 1 /* TEXT */ },
    ]
}

// 后
const nextBlock = {
    tag: 'div',
    dynamicChildren: [
        { tag: 'p', children: item, 1 /* TEXT */ },
        { tag: 'i', children: ctx.foo, 1 /* TEXT */ },
        { tag: 'i', children: ctx.bar, 1 /* TEXT */ },
    ]
}

prevBlcok 中有四個(gè)動態(tài)節(jié)點(diǎn),nextBlock 中有三個(gè)動態(tài)節(jié)點(diǎn)。這時(shí)候要如何進(jìn)行 Diff?有的同學(xué)可能會說拿 dynamicChildren 進(jìn)行傳統(tǒng) Diff,這是不對的,因?yàn)閭鹘y(tǒng) Diff 的一個(gè)前置條件是同層級節(jié)點(diǎn)間的 Diff,但是 dynamicChildren 內(nèi)的節(jié)點(diǎn)未必是同層級的,這一點(diǎn)我們之前就提到過。

實(shí)際上我們只需要讓 v-for 的元素也作為一個(gè) Block 就可以了。這樣無論 v-for 怎么變化,它始終都是一個(gè) Block,這保證了結(jié)構(gòu)穩(wěn)定,無論 v-for 怎么變化,這顆 Block Tree 看上去都是:

const block = {
    tag: 'div',
    dynamicChildren: [
        // 這是一個(gè) Block 哦,它有 dynamicChildren
        { tag: Fragment, dynamicChildren: [/*.. v-for 的節(jié)點(diǎn) ..*/] }
        { tag: 'i', children: ctx.foo, 1 /* TEXT */ },
        { tag: 'i', children: ctx.bar, 1 /* TEXT */ },
    ]
}

不穩(wěn)定的 Fragment

剛剛我們使用一個(gè) Fragment 并讓它充當(dāng) Block 的角色解決了 v-for 元素所在層級的結(jié)構(gòu)穩(wěn)定,但我們來看一下這個(gè) Fragment 本身:

{ tag: Fragment, dynamicChildren: [/*.. v-for 的節(jié)點(diǎn) ..*/] }

對于如下這樣的模板:

<p v-for="item in list">{{ item }}</p>

在 list 由 [1, 2] 變成 [1] 的前后,Fragment 這個(gè) Block 看上去應(yīng)該是:

// 前
const prevBlock = {
    tag: Fragment,
    dynamicChildren: [
        { tag: 'p', children: item, 1 /* TEXT */ },
        { tag: 'p', children: item, 2 /* TEXT */ }
    ]
}

// 后
const prevBlock = {
    tag: Fragment,
    dynamicChildren: [
        { tag: 'p', children: item, 1 /* TEXT */ }
    ]
}

我們發(fā)現(xiàn),Fragment 這個(gè) Block 仍然面臨結(jié)構(gòu)不穩(wěn)定的情況,所謂結(jié)構(gòu)不穩(wěn)定從結(jié)果上看指的是更新前后一個(gè) blockdynamicChildren 中收集的動態(tài)節(jié)點(diǎn)數(shù)量或順序的不一致。這種不一致會導(dǎo)致我們沒有辦法直接進(jìn)行靶向 Diff,怎么辦呢?其實(shí)對于這種情況是沒有辦法的,我們只能拋棄 dynamicChildrenDiff,并回退到傳統(tǒng) Diff:即 Diff Fragmentchildren 而非 dynamicChildren

但需要注意的是 Fragment 的子節(jié)點(diǎn)(children)仍然可以是 Block

const block = {
    tag: Fragment,
    children: [
        { tag: 'p', children: item, dynamicChildren: [/*...*/], 1 /* TEXT */ },
        { tag: 'p', children: item, dynamicChildren: [/*...*/], 1 /* TEXT */ }
    ]
}

這樣,對于 <p> 標(biāo)簽及其子代節(jié)點(diǎn)的 Diff 將恢復(fù) Block TreeDiff 模式。

穩(wěn)定的 Fragment

既然有不穩(wěn)定的 Fragment,那就有穩(wěn)定的 Fragment,什么樣的 Fragment 是穩(wěn)定的呢?

  • v-for 的表達(dá)式是常量
<p v-for="n in 10"></p>
<!-- 或者 -->
<p v-for="s in 'abc'"></p>

由于 10'abc' 是常量,所有這兩個(gè) Fragment 是不會變化的,因此它是穩(wěn)定的,對于穩(wěn)定的 Fragment 是不需要回退到傳統(tǒng) Diff 的,這在性能上會有一定的優(yōu)勢。

  • 多個(gè)根元素

Vue3 不再限制組件的模板必須有一個(gè)根節(jié)點(diǎn),對于多個(gè)根節(jié)點(diǎn)的模板,例如:

<template>
    <div></div>
    <p></p>
    <i></i>
</template>

如上,這也是一個(gè)穩(wěn)定的 Fragment,有的同學(xué)或許會想如下模板也是穩(wěn)定的 Fragment 嗎:

<template>
    <div v-if="condition"></div>
    <p></p>
    <i></i>
</template>

這其實(shí)也是穩(wěn)定的,因?yàn)閹в?v-if 指令的元素本身作為 Block 存在,所以這段模板的 Block Tree 結(jié)構(gòu)總是:

Block(Fragment)
    - Block(div v-if)
    - VNode(p)
    - VNode(i)

對應(yīng)到 VNode 應(yīng)該類似于:

const block = {
    tag: Fragment,
    dynamicChildren: [
        { tag: 'div', dynamicChildren: [...] },
        { tag: 'p' },
        { tag: 'i' },
    ],
    PatchFlags.STABLE_FRAGMENT
}

無論如何,它的結(jié)構(gòu)都是穩(wěn)定的。需要注意的是這里的 PatchFlags.STABLE_FRAGMENT,該標(biāo)志必須存在,否則會回退傳統(tǒng) Diff 模式。

  • 插槽出口

如下模板所示:

<Comp>
    <p v-if="ok"></p>
    <i v-else></i>
</Comp>

組件 <Comp> 內(nèi)的 children 將作為插槽內(nèi)容,在經(jīng)過編譯后,應(yīng)該作為 Block 角色的內(nèi)容自然會是 Block,已經(jīng)能夠保證結(jié)構(gòu)的穩(wěn)定了,例如如上代碼相當(dāng)于:

render(ctx) {
    return createVNode(Comp, null, {
        default: () => ([
            ctx.ok
                // 這里已經(jīng)是 Block 了
                ? (openBlock(), createBlock('p', { key: 0 }))
                : (openBlock(), createBlock('i', { key: 1 }))
        ]),
        _: 1 // 注意這里哦
    })
}

既然結(jié)構(gòu)已經(jīng)穩(wěn)定了,那么在渲染出口處 Comp.vue

<template>
    <slot/>
</template>

相當(dāng)于:

render() {
    return (openBlock(), createBlock(Fragment, null,
        this.$slots.default() || []
    ), PatchFlags.STABLE_FRAGMENT)
}

這自然就是 STABLE_FRAGMENT,大家注意前面代碼中 _: 1 這是一個(gè)編譯的 slot hint,當(dāng)我們手寫優(yōu)化模式的渲染函數(shù)時(shí)必須要使用這個(gè)標(biāo)志才能讓 runtime 知道 slot 是穩(wěn)定的,否則會退出非優(yōu)化模式。另外還有一個(gè) $stable hint,在文末會講解。

  • <template v-for>

如下模板所示:

<template>
    <template v-for="item in list">
        <p>{{ item.name }}</P>
        <p>{{ item.age }}</P>
    </template>
</template> 

對于帶有 v-fortemplate 元素本身來說,它是一個(gè)不穩(wěn)定的 Fragment,因?yàn)?list 不是常量。除此之外,由于 <template> 元素本身不渲染任何真實(shí) DOM,因此如果它含有多個(gè)元素節(jié)點(diǎn),那么這些元素節(jié)點(diǎn)也將作為 Fragment 存在,但這個(gè) Fragment 是穩(wěn)定的,因?yàn)樗粫S著 list 的變化而變化。

以上內(nèi)容差不多就是 Block Tree 配合 PatchFlags 是如何做到靶向更新以及一些具體的思路細(xì)節(jié)了。

靜態(tài)提升

提升靜態(tài)節(jié)點(diǎn)樹

Vue3Compiler 如果開啟了 hoistStatic 選項(xiàng)則會提升靜態(tài)節(jié)點(diǎn),或靜態(tài)的屬性,這可以減少創(chuàng)建 VNode 的消耗,如下模板所示:

<div>
    <p>text</p>
</div>

在沒有被提升的情況下其渲染函數(shù)相當(dāng)于:

function render() {
    return (openBlock(), createBlock('div', null, [
        createVNode('p', null, 'text')
    ]))
}

很明顯,p 標(biāo)簽是靜態(tài)的,它不會改變。但是如上渲染函數(shù)的問題也很明顯,如果組件內(nèi)存在動態(tài)的內(nèi)容,當(dāng)渲染函數(shù)重新執(zhí)行時(shí),即使 p 標(biāo)簽是靜態(tài)的,那么它對應(yīng)的 VNode 也會重新創(chuàng)建。當(dāng)開啟靜態(tài)提升后,其渲染函數(shù)如下:

const hoist1 = createVNode('p', null, 'text')

function render() {
    return (openBlock(), createBlock('div', null, [
        hoist1
    ]))
}

這就實(shí)現(xiàn)了減少 VNode 創(chuàng)建的性能消耗。需要了解的是,靜態(tài)提升是以樹為單位的,如下模板所示:

<div>
  <section>
    <p>
      <span>abc</span>
    </p>
  </section >
</div>

除了根節(jié)點(diǎn)的 div 作為 block 不可被提升之外,整個(gè) <section> 元素及其子代節(jié)點(diǎn)都會被提升,因?yàn)樗麄兪钦脴涠际庆o態(tài)的。如果我們把上面代碼中的 abc 換成 {{ abc }},那么整棵樹都不會被提升。再看如下代碼:

<div>
  <section>
    {{ dynamicText }}
    <p>
      <span>abc</span>
    </p>
  </section >
</div>

由于 section 標(biāo)簽內(nèi)包含動態(tài)插值,因此以 section 為根節(jié)點(diǎn)的子樹就不會被提升,但是 p 標(biāo)簽以及其子代節(jié)點(diǎn)都是靜態(tài)的,是可以被提升的。

元素不會被提升的情況

  • 元素帶有動態(tài)的 key 綁定

除了剛剛講到的元素的所有子代節(jié)點(diǎn)必須都是靜態(tài)的才會被提升之外還有哪些情況下會阻止提升呢?

如果一個(gè)元素有動態(tài)的 key 綁定那么它是不會被提升的,例如:

<div :key="foo"></div>

實(shí)際上一個(gè)元素?fù)碛腥魏蝿討B(tài)綁定都不應(yīng)該被提升,那么為什么 key 會被單獨(dú)拿出來?實(shí)際上 key 和普通的 props 相比,它對于 VNode 的意義是不一樣的,普通的 props 如果它是動態(tài)的,那么只需要體現(xiàn)在 PatchFlags 上就可以了,例如:

<div>
    <p :foo="bar"></p>
</div>

我們可以為 p 標(biāo)簽打上 PatchFlags

render(ctx) {
    return (openBlock(), createBlock('div', null, [
        createVNode('p', { foo: ctx }, null, PatchFlags.PROPS, ['foo'])
    ]))
}

注意到在創(chuàng)建 VNode 時(shí),為其打上了 PatchFlags.PROPS,代表這個(gè)元素需要更新 PROPS,并且需要更新的 PROPS 的名字叫 foo

h但是 key 本身具有特殊意hi義,它是 VNode(或元素) 的唯一標(biāo)識,即使兩個(gè)元素除了 key 以外一切都相同,但這兩個(gè)元素仍然是不同的元素,對于不同的元素需要做完全的替換處理才行,而 PatchFlags 用于在同一個(gè)元素上的屬性補(bǔ)丁,因此 key 是不同于其它 props 的。

正因?yàn)?key 的值是動態(tài)的可變的,因此對于擁有動態(tài) key 的元素,它始終都應(yīng)該參與到 diff 中并且不能簡單的打 PatchFlags 補(bǔ)丁標(biāo)識,那應(yīng)該怎么做呢?很簡單,讓擁有動態(tài) key 的元素也作為 Block 即可,以如下模板為例:

<div>
    <div :key="foo"></div>
</div>

它對應(yīng)的渲染函數(shù)應(yīng)該是:

render(ctx) {
    return (openBlock(), createBlock('div', null, [
        (openBlock(), createBlock('div', { key: ctx.foo }))
    ]))
}

Tips:手寫優(yōu)化模式的渲染函數(shù)時(shí),如果使用動態(tài)的 key,記得要使用 Block 哦,我們在后文還會總結(jié)。

  • 使用 ref 的元素

如果一個(gè)元素使用了 ref,無論是否動態(tài)綁定的值,那么這個(gè)元素都不會被靜態(tài)提升,這是因?yàn)樵诿恳淮?patch 時(shí)都需要設(shè)置 ref 的值,如下模板所示:

<div ref="domRef"></div>

乍一看覺得這完全就是一個(gè)靜態(tài)元素,沒錯(cuò),元素本身不會發(fā)生變化,但由于 ref 的特性,導(dǎo)致我們必須在每次 Diff 的過程中重新設(shè)置 ref 的值,為什么呢?來看一個(gè)使用 ref 的場景:

<template>
    <div>
        <p ref="domRef"></p>
    </div>
</template>
<script>
export default {
    setup() {
        const refP1 = ref(null)
        const refP2 = ref(null)
        const useP1 = ref(true)

        return {
            domRef: useP1 ? refP1 : refP2
        }
    }
}
</script>

如上代碼所示,p 標(biāo)簽使用了一個(gè)非動態(tài)的 ref 屬性,值為字符串 domRef,同時(shí)我們注意到 setupContext(我們把 setup 函數(shù)返回的對象叫做 setupContext) 中也包含了同名的 domRef 屬性,這不是偶然,他們之間會建立聯(lián)系,最終結(jié)果就是:

  • 當(dāng) useP1 為真時(shí),refP1.value 引用 p 元素
  • 當(dāng) useP1 為假時(shí),refP2.value 引用 p 元素

因此,即使 ref 是靜態(tài)的,但很顯然在更新的過程中由于 useP1 的變化,我們不得不更新 domRef,所以只要一個(gè)元素使用了 ref,它就不會被靜態(tài)提升,并且這個(gè)元素對應(yīng)的 VNode 也會被收集到父 BlockdynamicChildren 中。

但由于 p 標(biāo)簽除了需要更新 ref 之外,并不需要更新其他 props,所以在真實(shí)的渲染函數(shù)中,會為它打上一個(gè)特殊的 PatchFlag,叫做:PatchFlags.NEED_PATCH

render() {
    return (openBlock(), createBlock('div', null, [
        createVNode('p', { ref: 'domRef' }, null, PatchFlags.NEED_PATCH)
    ]))
}

  • 使用自定義指令的元素

實(shí)際上一個(gè)元素如果使用除 v-pre/v-cloak 之外的所有 Vue 原生提供的指令,都不會被提升,使用自定義指令也不會被提升,例如:

<p v-custom></p>

和使用 key 一樣,會為這段模板對應(yīng)的 VNode 打上 NEED_PATCH 標(biāo)志。順便講一下手寫渲染函數(shù)時(shí)如何應(yīng)用自定義指令,自定義指令是一種運(yùn)行時(shí)指令,與組件的生命周期類似,一個(gè) VNode 對象也有它自己生命周期:

  • beforeMount
  • mounted
  • beforeUpdate
  • updated
  • beforeUnmount
  • unmounted

編寫一個(gè)自定義指令:

const myDir: Directive = {
  beforeMount(el, binds) {
    console.log(el)
    console.log(binds.value)
    console.log(binds.oldValue)
    console.log(binds.arg)
    console.log(binds.modifiers)
    console.log(binds.instance)
  }
}

使用該指令:

const App = {
  setup() {
    return () => {
      return h('div', [
        // 調(diào)用 withDirectives 函數(shù)
        withDirectives(h('h1', 'hahah'), [
          // 四個(gè)參數(shù)分別是:指令、值、參數(shù)、修飾符
          [myDir, 10, 'arg', { foo: true }]
        ])
      ])
    }
  }
}

一個(gè)元素可以綁定多個(gè)指令:

const App = {
  setup() {
    return () => {
      return h('div', [
        // 調(diào)用 withDirectives 函數(shù)
        withDirectives(h('h1', 'hahah'), [
          // 四個(gè)參數(shù)分別是:指令、值、參數(shù)、修飾符
          [myDir, 10, 'arg', { foo: true }],
          [myDir2, 10, 'arg', { foo: true }],
          [myDir3, 10, 'arg', { foo: true }]
        ])
      ])
    }
  }
}

提升靜態(tài) PROPS

前面說過,靜態(tài)節(jié)點(diǎn)的提升以樹為單位,如果一個(gè) VNode 存在非靜態(tài)的子代節(jié)點(diǎn),那么該 VNode 就不是靜態(tài)的,也就不會被提升。但這個(gè) VNodeprops 卻可能是靜態(tài)的,這使我們可以將它的 props 進(jìn)行提升,這同樣可以節(jié)約 VNode 對象的創(chuàng)建開銷,內(nèi)存占用等,例如:

<div>
    <p foo="bar" a=b>{{ text }}</p>
</div>

在這段模板中 p 標(biāo)簽有動態(tài)的文本內(nèi)容,因此不可以被提升,但 p 標(biāo)簽的所有屬性都是靜態(tài)的,因此可以提升它的屬性,經(jīng)過提升后其渲染函數(shù)如下:

const hoistProp = { foo: 'bar', a: 'b' }

render(ctx) {
    return (openBlock(), createBlock('div', null, [
        createVNode('p', hoistProp, ctx.text)
    ]))
}

即使動態(tài)綁定的屬性值,但如果值是常量,那么也會被提升:

<p :foo="10" :bar="'abc' + 'def'">{{ text }}</p>

'abc' + 'def' 是常量,可以被提升。

預(yù)字符串化

靜態(tài)提升的 VNode 節(jié)點(diǎn)或節(jié)點(diǎn)樹本身是靜態(tài)的,那么能否將其預(yù)先字符串化呢?如下模板所示:

<div>
    <p></p>
    <p></p>
    ...20 個(gè) p 標(biāo)簽
    <p></p>
</div>

假設(shè)如上模板中有大量連續(xù)的靜態(tài)的 p 標(biāo)簽,當(dāng)采用了 hoist 優(yōu)化時(shí),結(jié)果如下:

cosnt hoist1 = createVNode('p', null, null, PatchFlags.HOISTED)
cosnt hoist2 = createVNode('p', null, null, PatchFlags.HOISTED)
... 20 個(gè) hoistx 變量
cosnt hoist20 = createVNode('p', null, null, PatchFlags.HOISTED)

render() {
    return (openBlock(), createBlock('div', null, [
        hoist1, hoist2, ...20 個(gè)變量, hoist20
    ]))
}

預(yù)字符串化會將這些靜態(tài)節(jié)點(diǎn)序列化為字符串并生成一個(gè) Static 類型的 VNode

const hoistStatic = createStaticVNode('<p></p><p></p><p></p>...20個(gè)...<p></p>')

render() {
    return (openBlock(), createBlock('div', null, [
       hoistStatic
    ]))
}

這有幾個(gè)明顯的優(yōu)勢:

  • 生成代碼的體積減少
  • 減少創(chuàng)建 VNode 的開銷
  • 減少內(nèi)存占用

靜態(tài)節(jié)點(diǎn)在運(yùn)行時(shí)會通過 innerHTML 來創(chuàng)建真實(shí)節(jié)點(diǎn),因此并非所有靜態(tài)節(jié)點(diǎn)都是可以預(yù)字符串化的,可以預(yù)字符串化的靜態(tài)節(jié)點(diǎn)需要滿足以下條件:

當(dāng)一個(gè)節(jié)點(diǎn)滿足這些條件時(shí)代表這個(gè)節(jié)點(diǎn)是可以預(yù)字符串化的,但是如果只有一個(gè)節(jié)點(diǎn),那么并不會將其字符串化,可字符串化的節(jié)點(diǎn)必須連續(xù)且達(dá)到一定數(shù)量才行:

  • 如果節(jié)點(diǎn)沒有屬性,那么必須有連續(xù) 20 個(gè)及以上的靜態(tài)節(jié)點(diǎn)存在才行,例如:
<div>
    <p></p>
    <p></p>
    ... 20 個(gè) p 標(biāo)簽
    <p></p>
</div>

或者在這些連續(xù)的節(jié)點(diǎn)中有 5 個(gè)及以上的節(jié)點(diǎn)是有屬性綁定的節(jié)點(diǎn):

<div>
    <p id="a"></p>
    <p id="b"></p>
    <p id="c"></p>
    <p id="d"></p>
    <p id="e"></p>
</div>

這段節(jié)點(diǎn)的數(shù)量雖然沒有達(dá)到 20 個(gè),但是滿足 5 個(gè)節(jié)點(diǎn)有屬性綁定。

這些節(jié)點(diǎn)不一定是兄弟關(guān)系,父子關(guān)系也是可以的,只要閾值滿足條件即可,例如:

<div>
    <p id="a">
        <p id="b">
            <p id="c">
                <p id="d">
                    <p id="e"></p>
                </p>
            </p>
        </p>
    </p>
</div>

預(yù)字符串化會在編譯時(shí)計(jì)算屬性的值,例如:

<div>
    <p :id="'id-' + 1">
        <p :id="'id-' + 2">
            <p :id="'id-' + 3">
                <p :id="'id-' + 4">
                    <p :id="'id-' + 5"></p>
                </p>
            </p>
        </p>
    </p>
</div>

在與字符串化之后:

const hoistStatic = createStaticVNode('<p id="id-1"></p><p id="id-2"></p>.....<p id="id-5"></p>')

可見 id 屬性值時(shí)計(jì)算后的。

Cache Event handler

如下組件的模板所示:

<Comp @change="a + b" />

這段模板如果手寫渲染函數(shù)的話相當(dāng)于:

render(ctx) {
    return h(Comp, {
        onChange: () => (ctx.a + ctx.b)
    })
}

很顯然,每次 render 函數(shù)執(zhí)行的時(shí)候,Comp 組件的 props 都是新的對象,onChange 也會是全新的函數(shù)。這會導(dǎo)致觸發(fā) Comp 組件的更新。

當(dāng) Vue3 Compiler 開啟 prefixIdentifiers 以及 cacheHandlers 時(shí),這段模板會被編譯為:

render(ctx, cache) {
    return h(Comp, {
        onChange: cache[0] || (cache[0] = ($event) => (ctx.a + ctx.b))
    })
}

這樣即使多次調(diào)用渲染函數(shù)也不會觸發(fā) Comp 組件的更新,因?yàn)?Vuepatch 階段比對 props 時(shí)就會發(fā)現(xiàn) onChange 的引用沒變。

如上代碼中 render 函數(shù)的 cache 對象是 Vue 內(nèi)部在調(diào)用渲染函數(shù)時(shí)注入的一個(gè)數(shù)組,像下面這種:

render.call(ctx, ctx, [])

實(shí)際上,我們即使不依賴編譯也能手寫出具備 cache 能力的代碼:

const Comp = {
    setup() {
        // 在 setup 中定義 handler
        const handleChange = () => {/* ... */}
        return () => {
            return h(AnthorComp, {
                onChange: handleChange  // 引用不變
            })
        }
    }
}

因此我們最好不要寫出如下這樣的代碼:

const Comp = {
    setup() {
        return () => {
            return h(AnthorComp, {
                onChang(){/*...*/}  // 每次渲染函數(shù)執(zhí)行,都是全新的函數(shù)
            })
        }
    }
}

v-once

這是 Vue2 就支持的功能,v-once 是一個(gè)“很指令”的指令,因?yàn)樗褪墙o編譯器看的,當(dāng)編譯器遇到 v-once 時(shí),會利用我們剛剛講過的 cache 來緩存全部或者一部分渲染函數(shù)的執(zhí)行結(jié)果,例如如下模板:

<div>
    <div v-once>{{ foo }}</div>
</div>

會被編譯為:

render(ctx, cache) {
    return (openBlock(), createBlock('div', null, [
        cache[1] || (cache[1] = h("div", null, ctx.foo, 1 /* TEXT */))
    ]))
}

這樣就緩存了這段 vnode。既然 vnode 已經(jīng)被緩存了,后續(xù)的更新就都會讀取緩存的內(nèi)容,而不會重新創(chuàng)建 vnode 對象了,同時(shí)在 Diff 的過程中也就不需要這段 vnode 參與了,因此你通常會看到編譯后的代碼更接近如下內(nèi)容:

render(ctx, cache) {
    return (openBlock(), createBlock('div', null, [
        cache[1] || (
            setBlockTracking(-1), // 阻止這段 VNode 被 Block 收集
            cache[1] = h("div", null, ctx.foo, 1 /* TEXT */),
            setBlockTracking(1), // 恢復(fù)
            cache[1] // 整個(gè)表達(dá)式的值
        )
    ]))
}

稍微解釋一下這段代碼,我們已經(jīng)講解過何為 “Block Tree”,而 openBlock()createBlock() 函數(shù)用來創(chuàng)建一個(gè) Block。而 setBlockTracking(-1) 則用來暫停收集的動作,所以在 v-once 編譯生成的代碼中你會看到它,這樣使用 v-once 包裹的內(nèi)容就不會被收集到父 Block 中,也就不參與 Diff 了。

所以,v-once 帶來的性能提升來自兩方面:

  • 1、VNode 的創(chuàng)建開銷
  • 2、無用的 Diff 開銷

但其實(shí)我們不通過模板編譯,一樣可以通過緩存 VNode 來減少 VNode 的創(chuàng)建開銷:

const Comp = {
    setup() {
        // 緩存 content
        const content = h('div', 'xxxx')
        return () => {
            return h('section', content)
        }
    }
}

但這樣避免不了無用的 Diff 開銷,因?yàn)槲覀儧]有使用 Block Tree 優(yōu)化模式。

這里有必要提及的一點(diǎn)是:在 Vue2.5.18+ 以及 Vue3 中 VNode 是可重用的,例如我們可以在不同的地方多次使用同一個(gè) VNode 節(jié)點(diǎn):

const Comp = {
    setup() {
        const content = h('div', 'xxxx')
        return () => {
            // 多次渲染 content
            return h('section', [content, content, content])
        }
    }
}

手寫高性能渲染函數(shù)

接下來我們將進(jìn)入重頭戲環(huán)節(jié),我們嘗試手寫優(yōu)化模式的渲染函數(shù)。

幾個(gè)需要記住的小點(diǎn):

  1. 一個(gè) Block 就是一個(gè)特殊的 VNode,可以理解為它只是比普通 VNode 多了一個(gè) dynamicChildren 屬性
  2. createBlock() 函數(shù)和 createVNode() 函數(shù)的調(diào)用簽名幾乎相同,實(shí)際上 createBlock() 函數(shù)內(nèi)部就是封裝了 createVNode(),這再次證明 Block 就是 VNode
  3. 在調(diào)用 createBlock() 創(chuàng)建 Block 前要先調(diào)用 openBlock() 函數(shù),通常這兩個(gè)函數(shù)配合逗號運(yùn)算符一同出現(xiàn):
render() {
    return (openBlock(), createBlock('div'))
}

Block Tree 是靈活的:

在之前的介紹中根節(jié)點(diǎn)以 Block 的角色存在的,但是根節(jié)點(diǎn)并不必須是 Block,我們可以在任意節(jié)點(diǎn)開啟 Block

setup() {
    return () => {
        return h('div', [
            (openBlock(), createBlock('p', null, [/*...*/]))
        ])
    }
}

這也是可以的,因?yàn)殇秩酒髟?Diff 的過程中如果 VNode 帶有 dynamicChildren 屬性,會自動進(jìn)入優(yōu)化模式。但是我們通常會讓根節(jié)點(diǎn)充當(dāng) Block 角色。

正確地使用 PatchFlags:

PatchFlags 用來標(biāo)記一個(gè)元素需要更新的內(nèi)容,例如當(dāng)元素有動態(tài)的 class 綁定時(shí),我們需要使用 PatchFlags.CLASS 標(biāo)記:

const App = {
  setup() {
    const refOk = ref(true)

    return () => {
      return (openBlock(), createBlock('div', null, [
        createVNode('p', { class: { foo: refOk.value } }, 'hello', PatchFlags.CLASS) // 使用 CLASS 標(biāo)記
      ]))
    }
  }
}

如果使用了錯(cuò)誤的標(biāo)記則可能導(dǎo)致更新失敗,下面列出詳細(xì)的標(biāo)記使用方式:

  • PatchFlags.CLASS - 當(dāng)有動態(tài)的 class 綁定時(shí)使用
  • PatchFlags.STYLE - 當(dāng)有動態(tài)的 style 綁定時(shí)使用,例如:
createVNode('p', { style: { color: refColor.value } }, 'hello', PatchFlags.STYLE)

  • PatchFlags.TEXT - 當(dāng)有動態(tài)的文本節(jié)點(diǎn)是使用,例如:
createVNode('p', null, refText.value, PatchFlags.TEXT)

  • PatchFlags.PROPS - 當(dāng)有除了 classstyle 之外的其他動態(tài)綁定屬性時(shí),例如:
createVNode('p', { foo: refVal.value }, 'hello', PatchFlags.PROPS, ['foo'])

這里需要注意的是,除了要使用 PatchFlags.PROPS 之外,還要提供第五個(gè)參數(shù),一個(gè)數(shù)組,包含了動態(tài)屬性的名字。

  • PatchFlags.FULL_PROPS - 當(dāng)有動態(tài) nameprops 時(shí)使用,例如:
createVNode('p', { [refKey.value]: 'val' }, 'hello', PatchFlags.FULL_PROPS)

實(shí)際上使用 FULL_PROPS 等價(jià)于對 propsDiff 與傳統(tǒng) Diff 一樣。其實(shí),如果覺得心智負(fù)擔(dān)大,我們大可以全部使用 FULL_PROPS,這么做的好處是:

  • 避免誤用 PatchFlags 導(dǎo)致的 bug
  • 減少心智負(fù)擔(dān)的同時(shí),雖然失去了 props diff 的性能優(yōu)勢,但是仍然可以享受 Block Tree 的優(yōu)勢。

當(dāng)同時(shí)存在多種更新,需要將 PatchFlags 進(jìn)行按位或運(yùn)算,例如:PatchFlags.CLASS | PatchFlags.STYLE

NEED_PATCH 標(biāo)識

為什么單獨(dú)把這個(gè)標(biāo)志拿出來講呢,它比較特殊,需要我們額外注意。當(dāng)我們使用 refonVNodeXXX 等 hook 時(shí)(包括自定義指令),需要使用該標(biāo)志,以至于它可以被父級 Block 收集,詳細(xì)原因我們在靜態(tài)提升一節(jié)里面講解過了:

const App = {
  setup() {
    const refDom = ref(null)
    return () => {
      return (openBlock(), createBlock('div', null,[
        createVNode('p',
          {
            ref: refDom,
            onVnodeBeforeMount() {/* ... */}
          },
          null,
          PatchFlags.NEED_PATCH
        )
      ]))
    }
  }
}

該使用 Block 的地方必須用

在最開始的時(shí)候,我們講解了有些指令會導(dǎo)致 DOM 結(jié)構(gòu)不穩(wěn)定,從而必須使用 Block 來解決問題。手寫渲染函數(shù)也是一樣:

  • 分支判斷使用 Block:
const App = {
  setup() {
    const refOk = ref(true)
    return () => {
      return (openBlock(), createBlock('div', null, [
        refOk.value
          // 這里使用 Block
          ? (openBlock(), createBlock('div', { key: 0 }, [/* ... */]))
          : (openBlock(), createBlock('div', { key: 1 }, [/* ... */]))
      ]))
    }
  }
}

這里使用 Block 的原因我們在前文已經(jīng)講解過了,但這里需要強(qiáng)調(diào)的是,除了分支判斷要使用 Block 之外,還需要為 Block 指定不同的 key 才行。

  • 列表使用 Block:

當(dāng)我們渲染列表時(shí),我們常常寫出如下代碼:

const App = {
  setup() {
    const obj = reactive({ list: [ { val: 1 }, { val: 2 } ] })

    return () => {
      return (openBlock(), createBlock('div', null,
        // 渲染列表
        obj.list.map(item => {
          return createVNode('p', null, item.val, PatchFlags.TEXT)
        })
      ))
    }
  }
}

這么寫在非優(yōu)化模式下是沒問題的,但我們現(xiàn)在使用了 Block,前文已經(jīng)講過為什么 v-for 需要使用 Block 的原因,試想當(dāng)我們執(zhí)行如下語句修改數(shù)據(jù):

obj.list.splice(0, 1)

這就會導(dǎo)致 Block 中收集的動態(tài)節(jié)點(diǎn)不一致,最終 Diff 出現(xiàn)問題。解決方案就是讓整個(gè)列表作為一個(gè) Block,這時(shí)我們需要使用 Fragment

const App = {
  setup() {
    const obj = reactive({ list: [ { val: 1 }, { val: 2 } ] })

    return () => {
      return (openBlock(), createBlock('div', null, [
        // 創(chuàng)建一個(gè) Fragment,并作為 Block 角色
        (openBlock(true), createBlock(Fragment, null,
          // 在這里渲染列表
          obj.list.map(item => {
            return createVNode('p', null, item.val, PatchFlags.TEXT)
          }),
          // 記得要指定正確的 PatchFlags
          PatchFlags.UNKEYED_FRAGMENT
        ))
      ]))
    }
  }
}

總結(jié)一下:

  • 對于列表我們應(yīng)該始終使用 Fragment,并作為 Block 的角色
  • 如果 Fragmentchildren 沒有指定 key,那么應(yīng)該為 Fragment 打上 PatchFlags.UNKEYED_FRAGMENT。相應(yīng)的,如果指定了 key 就應(yīng)該打上 PatchFlags.KEYED_FRAGMENT
  • 注意到在調(diào)用 openBlock(true) 時(shí),傳遞了參數(shù) true,這代表這個(gè) Block 不會收集 dynamicChildren,因?yàn)闊o論是 KEYED 還是 UNKEYEDFragment,在 Diff 它的 children 時(shí)都會回退傳統(tǒng) Diff 模式,因此不需要收集 dynamicChildren

這里還有一點(diǎn)需要注意,在 Diff Fragment 時(shí),由于回退了傳統(tǒng) Diff,我們希望盡快恢復(fù)優(yōu)化模式,同時(shí)保證后續(xù)收集的可控性,因此通常會讓 Fragment 的每一個(gè)子節(jié)點(diǎn)都作為 Block 的角色:

const App = {
  setup() {
    const obj = reactive({ list: [ { val: 1 }, { val: 2 } ] })

    return () => {
      return (openBlock(), createBlock('div', null, [
        (openBlock(true), createBlock(Fragment, null,
          obj.list.map(item => {
            // 修改了這里
            return (openBlock(), createBlock('p', null, item.val, PatchFlags.TEXT))
          }),
          PatchFlags.UNKEYED_FRAGMENT
        ))
      ]))
    }
  }
}

最后再說一下穩(wěn)定的 Fragment,如果你能確定列表永遠(yuǎn)不會變化,例如你能確定 obj.list 是不會變化的,那么你應(yīng)該使用:PatchFlags.STABLE_FRAGMENT 標(biāo)志,并且調(diào)用 openBlcok() 去掉參數(shù),代表收集 dynamicChildren

const App = {
  setup() {
    const obj = reactive({ list: [ { val: 1 }, { val: 2 } ] })

    return () => {
      return (openBlock(), createBlock('div', null, [
        // 調(diào)用 openBlock() 不要傳參
        (openBlock(), createBlock(Fragment, null,
          obj.list.map(item => {
            // 列表中的任何節(jié)點(diǎn)都不需要是 Block 角色
            return createVNode('p', null, item.val, PatchFlags.TEXT)
          }),
          // 穩(wěn)定的片段
          PatchFlags.STABLE_FRAGMENT
        ))
      ]))
    }
  }
}

如上注釋所述。

  • 使用動態(tài) key 的元素應(yīng)該是 Block

正如在靜態(tài)提升一節(jié)中所講的,當(dāng)元素使用動態(tài) key 的時(shí)候,即使兩個(gè)元素的其他方面完全一樣,那也是兩個(gè)不同的元素,需要做替換處理,在 Block Tree 中應(yīng)該以 Block 的角色存在,因此如果一個(gè)元素使用了動態(tài) key,它應(yīng)該是一個(gè) Block

const App = {
  setup() {
    const refKey = ref('foo')

    return () => {
      return (openBlock(), createBlock('div', null,[
        // 這里應(yīng)該是 Block
        (openBlock(), createBlock('p', { key: refKey.value }))
      ]))
    }
  }
}

這實(shí)際上是必須的,詳情查看 https://github.com/vuejs/vue-next/issues/938

使用 Slot hint

我們在“穩(wěn)定的 Fragment”一節(jié)中提到了 slot hint,當(dāng)我們?yōu)榻M件編寫插槽內(nèi)容時(shí),為了告訴 runtime:“我們已經(jīng)能夠保證插槽內(nèi)容的結(jié)構(gòu)穩(wěn)定”,則需要使用 slot hint

render() {
    return (openBlock(), createBlock(Comp, null, {
        default: () => [
            refVal.value
               ? (openBlock(), createBlock('p', ...)) 
               ? (openBlock(), createBlock('div', ...)) 
        ],
        // slot hint
        _: 1
    }))
}

當(dāng)然如果你不能保證這一點(diǎn),或者覺得心智負(fù)擔(dān)大,那么就不要寫 hint 了。

使用 $stable hint

$stable hint 和之前講的優(yōu)化策略不同,前文中的策略都是假設(shè)渲染器在優(yōu)化模式下工作的,而 $stable 用于非優(yōu)化模式,也就是我們平時(shí)寫的渲染函數(shù)。那么它有什么用呢?如下代碼所示(使用 tsx 演示):

export const App = defineComponent({
  name: 'App',
  setup() {
    const refVal = ref(true)

    return () => {
      refVal.value

      return (
        <Hello>
          {
            { default: () => [<p>hello</p>] }
          }
        </Hello>
      )
    }
  }
})

如上代碼所示,渲染函數(shù)中讀取了 refVal.value 的值,建立了依賴收集關(guān)系,當(dāng)修改 refVal 的值時(shí),會觸發(fā) <Hello> 組件的更新,但是我們發(fā)現(xiàn) Hello 組件一來沒有 props 變化,二來它的插槽內(nèi)容是靜態(tài)的,因此不應(yīng)該更新才對,這時(shí)我們可以使用 $stable hint

export const App = defineComponent({
  name: 'App',
  setup() {
    const refVal = ref(true)

    return () => {
      refVal.value

      return (
        <Hello>
          {
            { default: () => [<p>hello</p>], $stable: true } // 修改了這里
          }
        </Hello>
      )
    }
  }
})

為組件正確地使用 DYNAMIC_SLOTS

當(dāng)我們動態(tài)構(gòu)建 slots 時(shí),需要為組件的 VNode 指定 PatchFlags.DYNAMIC_SLOTS,否則將導(dǎo)致更新失敗。什么是動態(tài)構(gòu)建 slots 呢?通常情況下是指:依賴當(dāng)前 scope 變量構(gòu)建的 slots,例如:

render() {
    // 使用當(dāng)前組件作用域的變量
    const slots ={}
    // 常見的場景
    // 情況一:條件判斷
    if (refVal.value) {
        slots.header = () => [h('p', 'hello')]
    }
    // 情況二:循環(huán)
    refList.value.forEach(item => {
        slots[item.name] = () => [...]
    })
    // 情況三:動態(tài) slot 名稱,情況二包含情況三
    slots[refName.value] = () => [...]

    return (openBlock(), createBlock('div', null, [
        // 這里要使用 PatchFlags.DYNAMIC_SLOTS
        createVNode(Comp, null, slots, PatchFlags.DYNAMIC_SLOTS)
    ]))
}

如上注釋所述。

以上,不知道到達(dá)這里的同學(xué)有多少,Don't stop learning...

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容