antfu大佬的v-lazy-show教我學會了怎么編譯模板指令

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

看了下是兩天前的,所以好奇點進去看看是什么東東。
介紹是:
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).
簡單的說,v-lazy-show 是一個編譯時指令,就是對 v-show 的一種優(yōu)化,因為我們知道,v-show 的原理只是基于簡單的切換 display none,false則為none,true則移除
但即使在第一次條件為 falsy 的時候,其依然會渲染對應的組件,那如果該組件很大,就會帶來額外的渲染開銷,比如我們有個 Tabs,默認初始顯示第一個 tab,但后面的 tab 也都渲染了,只是沒有顯示罷了(實際上沒有必要,因為可能你點都不會點開)。
那基于此種情況下,我們可以優(yōu)化一下,即第一次條件為 falsy 的情況下,不渲染對應的組件,直到條件為 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 的情況下,對應 v-show 來說,其依然會渲染,而對于 v-lazy-show 或 v-show.lazy 來說,只有第一次 enabled 為 true 才渲染,避免了不必要的初始渲染開銷
如何使用?
國際慣例,先裝下依賴,這里強烈推薦 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
既然是個編譯時指令,且是處理 vue template 的,那么就應該在對應的構建工具中配置,如下:
如果你用的是 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,那么應該這樣配置:
// nuxt.config.ts
import { transformLazyShow } from 'v-lazy-show'
class="brush:js;"export default defineNuxtConfig({
vue: {
compilerOptions: {
nodeTransforms: [
transformLazyShow, // <--- 加上這行
],
},
},
})
那么,該指令是如何起作用的?
上面的指令作用很好理解,那么其是如何實現(xiàn)的呢?我們看下大佬是怎么做的。具體可見源碼
源碼不多,我這里直接貼出來,再一步步看如何實現(xiàn)(這里快速過一下即可,后面會一步步分析):
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
因為是處理運行時的指令,那么自然用到了 createStructuralDirectiveTransform 這個函數(shù),我們先簡單看下其作用:
createStructuralDirectiveTransform 是一個工廠函數(shù),用于創(chuàng)建一個自定義的 transform 函數(shù),用于在編譯過程中處理特定的結構性指令(例如 v-for, v-if, v-else-if, v-else 等)。
該函數(shù)有兩個參數(shù):
nameMatcher:一個正則表達式或字符串,用于匹配需要被處理的指令名稱。
fn:一個函數(shù),用于處理結構性指令。該函數(shù)有三個參數(shù):
- node:當前節(jié)點對象。
- dir:當前節(jié)點上的指令對象。
- context:編譯上下文對象,包含編譯期間的各種配置和數(shù)據(jù)。
createStructuralDirectiveTransform 函數(shù)會返回一個函數(shù),該函數(shù)接收一個節(jié)點對象和編譯上下文對象,用于根據(jù)指定的 nameMatcher 匹配到對應的指令后,調(diào)用用戶自定義的 fn 函數(shù)進行處理。
在編譯過程中,當遇到符合 nameMatcher 的結構性指令時,就會調(diào)用返回的處理函數(shù)進行處理,例如在本例中,當遇到 v-show 或 v-lazy-show 時,就會調(diào)用 transformLazyShow 處理函數(shù)進行處理。
不處理 v-show
if (dir.name === 'show' && !dir.modifiers.includes('lazy')) {
return () => {
node.props.push(dir)
}
}
因為 v-show.lazy 是可以生效的,所以 v-show 會進入該方法,但如果僅僅只是 v-show,而沒有 lazy 修飾符,那么實際上不用處理
這里有個細節(jié),為何要將指令對象 push 進 props,不 push 行不行?
原先的表現(xiàn)是 v-show 條件為 false 時 display 為 none,渲染了節(jié)點,只是不顯示:

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

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

這個跟 v-if 一樣
直到第一次條件為真時,將 _cache.${key} 置為 true,那么以后的行為就跟 v-show 一致了,上面的 dir.exp 即指令中的條件,如
<div v-show="enabled"/>
enabled 即 exp,表達式的意思。
readme給出的轉換如下:
<template>
<div v-lazy-show="foo">
Hello
</div>
</template>
會轉換為:
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)
}
你可以簡單理解為會將<ExpansiveComponent msg="v-lazy-show" v-lazy-show=""enabled"/>轉為下面:
<template v-if="_cache._lazyshow1 || enabled">
<!-- 為true時會把_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時會把_cache._lazyshow2置為true,那么以后的v-if就用于為true了 -->
<ExpansiveComponent msg="v-lazy-show" v-show.lazy="enabled"/>
</template>
<template v-else>
<!--v-show-if-->
</template>
然后將原先節(jié)點替換為處理后的 wrapperNode 即可
context.replaceNode(wrapNode)
最后將 v-lazy-show | v-shouw.lazy 處理為 v-show
因為 vue 本身是沒有 v-lazy-show 的,v-show 也沒有 lazy 的的修飾符,那么要讓指令生效,就要做到兩個:
- 將原先的 show-lazy 改名為 show
- 過濾掉 lazy 的修飾符
node.props.push({
...dir,
modifiers: dir.modifiers.filter(i => i !== 'lazy'),
name: 'show',
})
也就變成這樣啦:
<template v-if="_cache._lazyshow1 || enabled">
<!-- 為true時會把_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時會把_cache._lazyshow2置為true,那么以后的v-if就用于為true了 -->
<ExpansiveComponent msg="v-show.lazy" v-show="enabled"/>
</template>
<template v-else>
<!--v-show-if-->
</template>
小結一下:
為每一個使用 v-lazy-show 分配唯一的 key,放到渲染函數(shù)內(nèi)部的_cache上,即借助輔助變量_cache.${key}
- 當初始條件為 falsy 時不渲染節(jié)點,只渲染注釋節(jié)點
<!--v-show-if--> - 直到條件為真時將其置為 true,之后的表現(xiàn)就跟 v-show 一致了
- 由于 vue 不認識 v-lazy-show,v-show.lazy,使用要將指令改回 v-show,且過濾掉 lazy 修飾符(如果使用 v-show.lazy 的話)
最后
以上就是我對該運行時編譯插件的認識了,可以將 repo 拉下來,上面有個 playground,可以自己調(diào)試調(diào)試,說不定有新的認識。
好了,文章到此為止,你今天學廢了嗎?
以上就是antfu大佬的v-lazy-show,我學會了怎么編譯模板指令的詳細內(nèi)容,更多關于v-lazy-show編譯模板指令的資料請關注腳本之家其它相關文章!
相關文章
vue3+ts+echarts實現(xiàn)按需引入和類型界定方式
這篇文章主要介紹了vue3+ts+echarts實現(xiàn)按需引入和類型界定方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-10-10
laravel5.4+vue+element簡單搭建的示例代碼
本篇文章主要介紹了laravel5.4+vue+element簡單搭建的示例代碼,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-08-08

