antfu大佬的v-lazy-show教我學(xué)會(huì)了怎么編譯模板指令

前言
一開(kāi)始關(guān)注到 antfu 是他的一頭長(zhǎng)發(fā),畢竟留長(zhǎng)發(fā)的肯定是技術(shù)大佬。果不其然,antfu 是個(gè)很高產(chǎn)、很 creative 的大佬,我也很喜歡他寫(xiě)的工具,無(wú)論是@antfu/eslint-config、unocss、還是vitest等等。
而這篇文章故事的起源是,我今天中午逛 github 的時(shí)候發(fā)現(xiàn)大佬又又又又開(kāi)了一個(gè)新的 repo(這是家常便飯的事),v-lazy-show

看了下是兩天前的,所以好奇點(diǎn)進(jìn)去看看是什么東東。
介紹是:
A compile-time directive to lazy initialize v-show for Vue. It makes components mount after first truthy value (v-if), and the DOM keep alive when toggling (v-show).
簡(jiǎn)單的說(shuō),v-lazy-show 是一個(gè)編譯時(shí)指令,就是對(duì) v-show 的一種優(yōu)化,因?yàn)槲覀冎?,v-show 的原理只是基于簡(jiǎn)單的切換 display none,false則為none,true則移除
但即使在第一次條件為 falsy 的時(shí)候,其依然會(huì)渲染對(duì)應(yīng)的組件,那如果該組件很大,就會(huì)帶來(lái)額外的渲染開(kāi)銷(xiāo),比如我們有個(gè) Tabs,默認(rèn)初始顯示第一個(gè) tab,但后面的 tab 也都渲染了,只是沒(méi)有顯示罷了(實(shí)際上沒(méi)有必要,因?yàn)榭赡苣泓c(diǎn)都不會(huì)點(diǎn)開(kāi))。
那基于此種情況下,我們可以?xún)?yōu)化一下,即第一次條件為 falsy 的情況下,不渲染對(duì)應(yīng)的組件,直到條件為 truthy 才渲染該組件。
將原本的 v-show 改為 v-lazy-show 或者 v-show.lazy
<script setup lang="ts">
import { ref } from 'vue'
import ExpansiveComponent from './ExpansiveComponent.vue'
class="brush:js;"const enabled = ref(false)
</script>
class="brush:js;"<template>
<button @click="enabled = !enabled">
Toggle
</button>
class="brush:js;" <div class="hello-word-wrapper">
<ExpansiveComponent v-lazy-show="enabled" msg="v-lazy-show" />
<ExpansiveComponent v-show.lazy="enabled" msg="v-lazy.show" />
class="brush:js;" <ExpansiveComponent v-show="enabled" msg="v-show" />
class="brush:js;" <ExpansiveComponent v-if="enabled" msg="v-if" />
</div>
</template>
<!-- ExpansiveComponent.vue -->
<script setup lang="ts">
import { onMounted } from 'vue'
class="brush:js;"const props = defineProps({
msg: {
type: String,
required: true,
},
})
class="brush:js;"onMounted(() => {
console.log(`${props.msg} mounted`)
})
</script>
class="brush:js;"<template>
<div>
<div v-for="i in 1000" :key="i">
Hello {{ msg }}
</div>
</div>
</template>

ExpansiveComponent 渲染了 1000 行 div,在條件 enabled 初始為 false 的情況下,對(duì)應(yīng) v-show 來(lái)說(shuō),其依然會(huì)渲染,而對(duì)于 v-lazy-show 或 v-show.lazy 來(lái)說(shuō),只有第一次 enabled 為 true 才渲染,避免了不必要的初始渲染開(kāi)銷(xiāo)
如何使用?
國(guó)際慣例,先裝下依賴(lài),這里強(qiáng)烈推薦 antfu 大佬的 ni。
npm install v-lazy-show -D yarn add v-lazy-show -D pnpm add v-lazy-show -D ni v-lazy-show -D
既然是個(gè)編譯時(shí)指令,且是處理 vue template 的,那么就應(yīng)該在對(duì)應(yīng)的構(gòu)建工具中配置,如下:
如果你用的是 vite,那么配置如下
// vite.config.ts
import { defineConfig } from 'vite'
import { transformLazyShow } from 'v-lazy-show'
class="brush:js;"export default defineConfig({
plugins: [
Vue({
template: {
compilerOptions: {
nodeTransforms: [
transformLazyShow, // <--- 加在這里
],
},
},
}),
]
})
如果你用的是 Nuxt,那么應(yīng)該這樣配置:
// nuxt.config.ts
import { transformLazyShow } from 'v-lazy-show'
class="brush:js;"export default defineNuxtConfig({
vue: {
compilerOptions: {
nodeTransforms: [
transformLazyShow, // <--- 加上這行
],
},
},
})
那么,該指令是如何起作用的?
上面的指令作用很好理解,那么其是如何實(shí)現(xiàn)的呢?我們看下大佬是怎么做的。具體可見(jiàn)源碼
源碼不多,我這里直接貼出來(lái),再一步步看如何實(shí)現(xiàn)(這里快速過(guò)一下即可,后面會(huì)一步步分析):
import {
CREATE_COMMENT,
FRAGMENT,
createCallExpression,
createCompoundExpression,
createConditionalExpression,
createSequenceExpression,
createSimpleExpression,
createStructuralDirectiveTransform,
createVNodeCall,
traverseNode,
} from '@vue/compiler-core'
class="brush:js;"const indexMap = new WeakMap()
class="brush:js;"http:// https://github.com/vuejs/core/blob/f5971468e53683d8a54d9cd11f73d0b95c0e0fb7/packages/compiler-core/src/ast.ts#L28
const NodeTypes = {
SIMPLE_EXPRESSION: 4,
}
class="brush:js;"http:// https://github.com/vuejs/core/blob/f5971468e53683d8a54d9cd11f73d0b95c0e0fb7/packages/compiler-core/src/ast.ts#L62
const ElementTypes = {
TEMPLATE: 3,
}
class="brush:js;"http:// https://github.com/vuejs/core/blob/f5971468e53683d8a54d9cd11f73d0b95c0e0fb7/packages/shared/src/patchFlags.ts#L19
const PatchFlags = {
STABLE_FRAGMENT: 64,
}
class="brush:js;"export const transformLazyShow = createStructuralDirectiveTransform(
/^(lazy-show|show)$/,
(node, dir, context) => {
// forward normal `v-show` as-is
if (dir.name === 'show' && !dir.modifiers.includes('lazy')) {
return () => {
node.props.push(dir)
}
}
class="brush:js;" const directiveName = dir.name === 'show'
? 'v-show.lazy'
: 'v-lazy-show'
class="brush:js;" if (node.tagType === ElementTypes.TEMPLATE || node.tag === 'template')
throw new Error(`${directiveName} can not be used on <template>`)
class="brush:js;" if (context.ssr || context.inSSR) {
// rename `v-lazy-show` to `v-if` in SSR, and let Vue handles it
node.props.push({
...dir,
exp: dir.exp
? createSimpleExpression(dir.exp.loc.source)
: undefined,
modifiers: dir.modifiers.filter(i => i !== 'lazy'),
name: 'if',
})
return
}
class="brush:js;" const { helper } = context
const keyIndex = (indexMap.get(context.root) || 0) + 1
indexMap.set(context.root, keyIndex)
class="brush:js;" const key = `_lazyshow${keyIndex}`
class="brush:js;" const body = createVNodeCall(
context,
helper(FRAGMENT),
undefined,
[node],
PatchFlags.STABLE_FRAGMENT.toString(),
undefined,
undefined,
true,
false,
false /* isComponent */,
node.loc,
)
class="brush:js;" const wrapNode = createConditionalExpression(
createCompoundExpression([`_cache.${key}`, ' || ', dir.exp!]),
createSequenceExpression([
createCompoundExpression([`_cache.${key} = true`]),
body,
]),
createCallExpression(helper(CREATE_COMMENT), [
'"v-show-if"',
'true',
]),
) as any
class="brush:js;" context.replaceNode(wrapNode)
class="brush:js;" return () => {
if (!node.codegenNode)
traverseNode(node, context)
class="brush:js;" // rename `v-lazy-show` to `v-show` and let Vue handles it
node.props.push({
...dir,
modifiers: dir.modifiers.filter(i => i !== 'lazy'),
name: 'show',
})
}
},
)
createStructuralDirectiveTransform
因?yàn)槭翘幚磉\(yùn)行時(shí)的指令,那么自然用到了 createStructuralDirectiveTransform 這個(gè)函數(shù),我們先簡(jiǎn)單看下其作用:
createStructuralDirectiveTransform 是一個(gè)工廠函數(shù),用于創(chuàng)建一個(gè)自定義的 transform 函數(shù),用于在編譯過(guò)程中處理特定的結(jié)構(gòu)性指令(例如 v-for, v-if, v-else-if, v-else 等)。
該函數(shù)有兩個(gè)參數(shù):
nameMatcher:一個(gè)正則表達(dá)式或字符串,用于匹配需要被處理的指令名稱(chēng)。
fn:一個(gè)函數(shù),用于處理結(jié)構(gòu)性指令。該函數(shù)有三個(gè)參數(shù):
- node:當(dāng)前節(jié)點(diǎn)對(duì)象。
- dir:當(dāng)前節(jié)點(diǎn)上的指令對(duì)象。
- context:編譯上下文對(duì)象,包含編譯期間的各種配置和數(shù)據(jù)。
createStructuralDirectiveTransform 函數(shù)會(huì)返回一個(gè)函數(shù),該函數(shù)接收一個(gè)節(jié)點(diǎn)對(duì)象和編譯上下文對(duì)象,用于根據(jù)指定的 nameMatcher 匹配到對(duì)應(yīng)的指令后,調(diào)用用戶(hù)自定義的 fn 函數(shù)進(jìn)行處理。
在編譯過(guò)程中,當(dāng)遇到符合 nameMatcher 的結(jié)構(gòu)性指令時(shí),就會(huì)調(diào)用返回的處理函數(shù)進(jìn)行處理,例如在本例中,當(dāng)遇到 v-show 或 v-lazy-show 時(shí),就會(huì)調(diào)用 transformLazyShow 處理函數(shù)進(jìn)行處理。
不處理 v-show
if (dir.name === 'show' && !dir.modifiers.includes('lazy')) {
return () => {
node.props.push(dir)
}
}
因?yàn)?v-show.lazy 是可以生效的,所以 v-show 會(huì)進(jìn)入該方法,但如果僅僅只是 v-show,而沒(méi)有 lazy 修飾符,那么實(shí)際上不用處理
這里有個(gè)細(xì)節(jié),為何要將指令對(duì)象 push 進(jìn) props,不 push 行不行?
原先的表現(xiàn)是 v-show 條件為 false 時(shí) display 為 none,渲染了節(jié)點(diǎn),只是不顯示:

而注釋node.props.push(dir)后,看看頁(yè)面表現(xiàn)咋樣:

v-show 的功能沒(méi)了,也就是說(shuō)指令的功能會(huì)添加到 props 上,所以這里要特別注意,不是單純的返回 node 即可。后來(lái)還有幾處node.props.push,原理跟這里一樣。
服務(wù)端渲染目前是轉(zhuǎn)為 v-if
if (context.ssr || context.inSSR) {
// rename `v-lazy-show` to `v-if` in SSR, and let Vue handles it
node.props.push({
...dir,
exp: dir.exp
? createSimpleExpression(dir.exp.loc.source)
: undefined,
modifiers: dir.modifiers.filter(i => i !== 'lazy'),
name: 'if',
})
return
}
將 v-lazy-show 改名為 v-if,且過(guò)濾掉修飾符
createVNodeCall 給原先節(jié)點(diǎn)包一層 template
顧名思義,createVNodeCall 是 用來(lái)創(chuàng)建一個(gè) vnode 節(jié)點(diǎn)的函數(shù):
const body = createVNodeCall(
/** 當(dāng)前的上下文 (context) 對(duì)象,即 CodegenContext */
context,
/** helper 函數(shù)是 Vue 內(nèi)部使用的幫助函數(shù)。FRAGMENT 表示創(chuàng)建 Fragment 節(jié)點(diǎn)的 helper 函數(shù) */
helper(FRAGMENT),
/** 組件的 props */
undefined,
/** 當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn)數(shù)組,即包含有指令的節(jié)點(diǎn)本身 */
[node],
/** 表示該節(jié)點(diǎn)的 PatchFlag,指明了該節(jié)點(diǎn)是否穩(wěn)定、是否具有一些特定的更新行為等。STABLE_FRAGMENT 表示該 Fragment 節(jié)點(diǎn)是一個(gè)穩(wěn)定的節(jié)點(diǎn),即其子節(jié)點(diǎn)不會(huì)發(fā)生改變 */
PatchFlags.STABLE_FRAGMENT.toString(),
/** 該節(jié)點(diǎn)的動(dòng)態(tài) keys */
undefined,
/** 該節(jié)點(diǎn)的模板引用 (ref) */
undefined,
/** 表示該節(jié)點(diǎn)是否需要開(kāi)啟 Block (塊) 模式,即是否需要對(duì)其子節(jié)點(diǎn)進(jìn)行優(yōu)化 */
true,
/** 表示該節(jié)點(diǎn)是否是一個(gè) Portal 節(jié)點(diǎn) */
false,
/** 表示該節(jié)點(diǎn)是否是一個(gè)組件 */
false /* isComponent */,
/** 該節(jié)點(diǎn)在模板中的位置信息 */
node.loc,
)
參數(shù)含義如下,簡(jiǎn)單了解即可(反正看了就忘)
也就是說(shuō),其會(huì)生成如下模板:
<template> <ExpansiveComponent v-lazy-show="enabled" msg="v-lazy-show" /> </template>
關(guān)鍵代碼(重點(diǎn))
接下來(lái)這部分是主要原理,請(qǐng)打起十二分精神。
先在全局維護(hù)一個(gè) map,代碼中叫 indexMap,是一個(gè) WeakMap(不知道 WeakMap 的可以去了解下)。然后為每一個(gè)帶有 v-lazy-show 指令的生成一個(gè)唯一 key,這里叫做_lazyshow${keyIndex},也就是第一個(gè)就是_lazyshow1,第二個(gè)是_lazyshow2...
const keyIndex = (indexMap.get(context.root) || 0) + 1
indexMap.set(context.root, keyIndex)
class="brush:js;" const key = `_lazyshow${keyIndex}`
然后將生成的key放到渲染函數(shù)的_cache上(渲染函數(shù)的第二個(gè)參數(shù),function render(_ctx, _cache)),即通過(guò)_cache.${key}作為輔助變量。之后會(huì)根據(jù) createConditionalExpression 創(chuàng)建一個(gè)條件表達(dá)式
const wrapNode = createConditionalExpression(
createCompoundExpression([`_cache.${key}`, ' || ', dir.exp!]),
createSequenceExpression([
createCompoundExpression([`_cache.${key} = true`]),
body,
]),
// 生成一個(gè)注釋節(jié)點(diǎn) `<!--v-show-if-->`
createCallExpression(helper(CREATE_COMMENT), [
'"v-show-if"',
'true',
]),
)
也就是說(shuō), v-lazy-show 初始傳入的條件為 false 時(shí),那么會(huì)為你創(chuàng)建一個(gè)注釋節(jié)點(diǎn),用來(lái)占位:
createCallExpression(helper(CREATE_COMMENT), [ '"v-show-if"', 'true', ])

這個(gè)跟 v-if 一樣
直到第一次條件為真時(shí),將 _cache.${key} 置為 true,那么以后的行為就跟 v-show 一致了,上面的 dir.exp 即指令中的條件,如
<div v-show="enabled"/>
enabled 即 exp,表達(dá)式的意思。
readme給出的轉(zhuǎn)換如下:
<template>
<div v-lazy-show="foo">
Hello
</div>
</template>
會(huì)轉(zhuǎn)換為:
import { Fragment as _Fragment, createCommentVNode as _createCommentVNode, createElementBlock as _createElementBlock, createElementVNode as _createElementVNode, openBlock as _openBlock, vShow as _vShow, withDirectives as _withDirectives } from 'vue'
class="brush:js;"export function render(_ctx, _cache) {
return (_cache._lazyshow1 || _ctx.foo)
? (_cache._lazyshow1 = true, (_openBlock(),
_withDirectives(_createElementVNode('div', null, ' Hello ', 512 /* NEED_PATCH */), [
[_vShow, _ctx.foo]
])))
: _createCommentVNode('v-show-if', true)
}
你可以簡(jiǎn)單理解為會(huì)將<ExpansiveComponent msg="v-lazy-show" v-lazy-show=""enabled"/>轉(zhuǎn)為下面:
<template v-if="_cache._lazyshow1 || enabled">
<!-- 為true時(shí)會(huì)把_cache._lazyshow1置為true,那么以后的v-if就用于為true了 -->
<ExpansiveComponent msg="v-lazy-show" v-lazy-show="enabled"/>
</template>
<template v-else>
<!--v-show-if-->
</template>
class="brush:js;"<template v-if="_cache._lazyshow2 || enabled">
<!-- 為true時(shí)會(huì)把_cache._lazyshow2置為true,那么以后的v-if就用于為true了 -->
<ExpansiveComponent msg="v-lazy-show" v-show.lazy="enabled"/>
</template>
<template v-else>
<!--v-show-if-->
</template>
然后將原先節(jié)點(diǎn)替換為處理后的 wrapperNode 即可
context.replaceNode(wrapNode)
最后將 v-lazy-show | v-shouw.lazy 處理為 v-show
因?yàn)?vue 本身是沒(méi)有 v-lazy-show 的,v-show 也沒(méi)有 lazy 的的修飾符,那么要讓指令生效,就要做到兩個(gè):
- 將原先的 show-lazy 改名為 show
- 過(guò)濾掉 lazy 的修飾符
node.props.push({
...dir,
modifiers: dir.modifiers.filter(i => i !== 'lazy'),
name: 'show',
})
也就變成這樣啦:
<template v-if="_cache._lazyshow1 || enabled">
<!-- 為true時(shí)會(huì)把_cache._lazyshow1置為true,那么以后的v-if就用于為true了 -->
<ExpansiveComponent msg="v-lazy-show" v-show="enabled"/>
</template>
<template v-else>
<!--v-show-if-->
</template>
<template v-if="_cache._lazyshow2 || enabled">
<!-- 為true時(shí)會(huì)把_cache._lazyshow2置為true,那么以后的v-if就用于為true了 -->
<ExpansiveComponent msg="v-show.lazy" v-show="enabled"/>
</template>
<template v-else>
<!--v-show-if-->
</template>
小結(jié)一下:
為每一個(gè)使用 v-lazy-show 分配唯一的 key,放到渲染函數(shù)內(nèi)部的_cache上,即借助輔助變量_cache.${key}
- 當(dāng)初始條件為 falsy 時(shí)不渲染節(jié)點(diǎn),只渲染注釋節(jié)點(diǎn)
<!--v-show-if--> - 直到條件為真時(shí)將其置為 true,之后的表現(xiàn)就跟 v-show 一致了
- 由于 vue 不認(rèn)識(shí) v-lazy-show,v-show.lazy,使用要將指令改回 v-show,且過(guò)濾掉 lazy 修飾符(如果使用 v-show.lazy 的話)
最后
以上就是我對(duì)該運(yùn)行時(shí)編譯插件的認(rèn)識(shí)了,可以將 repo 拉下來(lái),上面有個(gè) playground,可以自己調(diào)試調(diào)試,說(shuō)不定有新的認(rèn)識(shí)。
好了,文章到此為止,你今天學(xué)廢了嗎?
以上就是antfu大佬的v-lazy-show,我學(xué)會(huì)了怎么編譯模板指令的詳細(xì)內(nèi)容,更多關(guān)于v-lazy-show編譯模板指令的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
unplugin-auto-import與unplugin-vue-components安裝問(wèn)題解析
這篇文章主要為大家介紹了unplugin-auto-import與unplugin-vue-components問(wèn)題解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02
vue?使用addRoutes動(dòng)態(tài)添加路由及刷新頁(yè)面跳轉(zhuǎn)404路由的問(wèn)題解決方案
我自己使用addRoutes動(dòng)態(tài)添加的路由頁(yè)面,使用router-link標(biāo)簽可以跳轉(zhuǎn),但是一刷新就會(huì)自動(dòng)跳轉(zhuǎn)到我定義的通配符?*?指向的404路由頁(yè)面,這說(shuō)明沒(méi)有找到指定路由才跳到404路由的,這樣的情況如何處理呢,下面小編給大家分享解決方案,一起看看吧2023-10-10
Vue 開(kāi)發(fā)必須知道的36個(gè)技巧(小結(jié))
這篇文章主要介紹了Vue 開(kāi)發(fā)必須知道的36個(gè)技巧(小結(jié)),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10
Vue富文本插件(quill-editor)的使用及說(shuō)明
這篇文章主要介紹了Vue富文本插件(quill-editor)的使用及說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。2023-02-02
Vue支持搜索與篩選的用戶(hù)列表實(shí)現(xiàn)流程介紹
這篇文章主要介紹了Vue支持搜索與篩選的用戶(hù)列表實(shí)現(xiàn)流程,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧2022-10-10
nuxt 每個(gè)頁(yè)面head標(biāo)簽內(nèi)容設(shè)置方式
這篇文章主要介紹了nuxt 每個(gè)頁(yè)面head標(biāo)簽內(nèi)容設(shè)置方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-11-11

