前言
一开始关注到 antfu 是他的一头长发,毕竟留长发的肯定是技术大佬。果不其然,antfu 是个很高产、很 creative 的大佬,我也很喜欢他写的工具,无论是@antfu/eslint-config、unocss、还是vitest等等。
而这篇文章故事的起源是,我今天中午逛 github 的时候发现大佬又又又又开了一个新的 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 的一种优化,因为我们知道,v-show 的原理只是基于简单的切换 display none,false则为none,true则移除
但即使在第一次条件为 falsy 的时候,其依然会渲染对应的组件,那如果该组件很大,就会带来额外的渲染开销,比如我们有个 Tabs,默认初始显示第一个 tab,但后面的 tab 也都渲染了,只是没有显示罢了(实际上没有必要,因为可能你点都不会点开)。
那基于此种情况下,我们可以优化一下,即第一次条件为 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, // <--- 加上这行
],
},
},
})
那么,该指令是如何起作用的?
上面的指令作用很好理解,那么其是如何实现的呢?我们看下大佬是怎么做的。具体可见源码
源码不多,我这里直接贴出来,再一步步看如何实现(这里快速过一下即可,后面会一步步分析):
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;"// https://github.com/vuejs/core/blob/f5971468e53683d8a54d9cd11f73d0b95c0e0fb7/packages/compiler-core/src/ast.ts#L28
const NodeTypes = {
SIMPLE_EXPRESSION: 4,
}
class="brush:js;"// https://github.com/vuejs/core/blob/f5971468e53683d8a54d9cd11f73d0b95c0e0fb7/packages/compiler-core/src/ast.ts#L62
const ElementTypes = {
TEMPLATE: 3,
}
class="brush:js;"// 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 ,
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 这个函数,我们先简单看下其作用:
createStructuralDirectiveTransform 是一个工厂函数,用于创建一个自定义的 transform 函数,用于在编译过程中处理特定的结构性指令(例如 v-for, v-if, v-else-if, v-else 等)。
该函数有两个参数:
nameMatcher:一个正则表达式或字符串,用于匹配需要被处理的指令名称。
fn:一个函数,用于处理结构性指令。该函数有三个参数:
- node:当前节点对象。
- dir:当前节点上的指令对象。
- context:编译上下文对象,包含编译期间的各种配置和数据。
createStructuralDirectiveTransform 函数会返回一个函数,该函数接收一个节点对象和编译上下文对象,用于根据指定的 nameMatcher 匹配到对应的指令后,调用用户自定义的 fn 函数进行处理。
在编译过程中,当遇到符合 nameMatcher 的结构性指令时,就会调用返回的处理函数进行处理,例如在本例中,当遇到 v-show 或 v-lazy-show 时,就会调用 transformLazyShow 处理函数进行处理。
不处理 v-show
if (dir.name === 'show' && !dir.modifiers.includes('lazy')) {
return () => {
node.props.push(dir)
}
}
因为 v-show.lazy 是可以生效的,所以 v-show 会进入该方法,但如果仅仅只是 v-show,而没有 lazy 修饰符,那么实际上不用处理
这里有个细节,为何要将指令对象 push 进 props,不 push 行不行?
原先的表现是 v-show 条件为 false 时 display 为 none,渲染了节点,只是不显示:
而注释node.props.push(dir)
后,看看页面表现咋样:
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 给原先节点包一层 template
顾名思义,createVNodeCall 是 用来创建一个 vnode 节点的函数:
const body = createVNodeCall(
context,
helper(FRAGMENT),
undefined,
[node],
PatchFlags.STABLE_FRAGMENT.toString(),
undefined,
undefined,
true,
false,
false ,
node.loc,
)
参数含义如下,简单了解即可(反正看了就忘)
也就是说,其会生成如下模板:
<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放到渲染函数的_cache上(渲染函数的第二个参数,function render(_ctx, _cache)
),即通过_cache.${key}
作为辅助变量。之后会根据 createConditionalExpression 创建一个条件表达式
const wrapNode = createConditionalExpression(
createCompoundExpression([`_cache.${key}`, ' || ', dir.exp!]),
createSequenceExpression([
createCompoundExpression([`_cache.${key} = true`]),
body,
]),
// 生成一个注释节点 `<!--v-show-if-->`
createCallExpression(helper(CREATE_COMMENT), [
'"v-show-if"',
'true',
]),
)
也就是说, v-lazy-show 初始传入的条件为 false 时,那么会为你创建一个注释节点,用来占位:
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 ), [
[_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>
然后将原先节点替换为处理后的 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,放到渲染函数内部的_cache上,即借助辅助变量_cache.${key}
- 当初始条件为 falsy 时不渲染节点,只渲染注释节点
<!--v-show-if-->
- 直到条件为真时将其置为 true,之后的表现就跟 v-show 一致了
- 由于 vue 不认识 v-lazy-show,v-show.lazy,使用要将指令改回 v-show,且过滤掉 lazy 修饰符(如果使用 v-show.lazy 的话)
最后
以上就是我对该运行时编译插件的认识了,可以将 repo 拉下来,上面有个 playground,可以自己调试调试,说不定有新的认识。
好了,文章到此为止,你今天学废了吗?
以上就是antfu大佬的v-lazy-show,我学会了怎么编译模板指令的详细内容,更多关于v-lazy-show编译模板指令的资料请关注编程网其它相关文章!