Vue3 中 v-if 和 v-show 指令实现的原理 | 源码解读
前言
又回到了经典的一句话:“知其然,而后使其然”。相信大家对 Vue 提供 和 指令的使用以及对应场景应该都滚瓜烂熟了。但是,我想仍然会有很多同学对于 和 指令实现的原理存在知识空白。
所以,今天就让我们来一起了解一番 和 指令实现的原理~
v-if
在之前 一文中,我给大家介绍了 Vue 3 的编译过程,即一个模版会经历 、、 这三个过程,最后由 生成可以执行的代码( 函数)。
这里,我们就不从编译过程开始讲解 指令的 函数生成过程了,有兴趣了解这个过程的同学,可以看我之前的文章~
然后,由它编译生成的 函数会是这样:
render(_ctx, _cache, $props, $setup, $data, $options) {
return (_ctx.visible)
? (_openBlock(), _createBlock("div", { key: 0 }))
: _createCommentVNode("v-if", true)
}
可以看到,一个简单的使用 指令的模版编译生成的 函数最终会返回一个三目运算表达式。首先,让我们先来认识一下其中几个变量和函数的意义:
当前组件实例的上下文,即
和 用于构造 和 ,它们主要用于靶向更新过程
创建注释节点的函数,通常用于占位
显然,如果当 为 的时候,会在当前模版中创建一个注释节点(也可称为占位节点),反之则创建一个真实节点(即它自己)。例如当 为 时渲染到页面上会是这样:

在 Vue 中很多地方都运用了注释节点来作为占位节点,其目的是在不展示该元素的时候,标识其在页面中的位置,以便在 的时候将该元素放回该位置。
那么,这个时候我想大家就会抛出一个疑问:当 动态切换 或 的这个过程(派发更新)究竟发生了什么?
派发更新时 patch,更新节点
在 Vue 3 中总共有四种指令:、、 和 。但是,实际上在源码中,只针对前面三者**进行了特殊处理**,这可以在 目录下的文件看出:
// packages/runtime-dom/src/directives
|-- driectives
|-- vModel.ts ## v-model 指令相关
|-- vOn.ts ## v-on 指令相关
|-- vShow.ts ## v-show 指令相关
而针对 指令是直接走派发更新过程时 的逻辑。由于 指令订阅了 变量,所以当 变化的时候,则会触发**派发更新**,即 对象的 逻辑,最后会命中 的逻辑。
当然,我们也可以称这个过程为组件的更新过程
这里,我们来看一下 的定义(伪代码):
// packages/runtime-core/src/renderer.ts
function componentEffect() {
if (!instance.isMounted) {
....
} else {
...
const nextTree = renderComponentRoot(instance)
const prevTree = instance.subTree
instance.subTree = nextTree
patch(
prevTree,
nextTree,
hostParentNode(prevTree.el!)!,
getNextHostNode(prevTree),
instance,
parentSuspense,
isSVG
)
...
}
}
}
可以看到,当组件还没挂载时,即第一次触发派发更新会命中 的逻辑。而对于我们这个栗子,则会命中 的逻辑,即组件更新,主要会做三件事:
获取当前组件对应的组件树 和之前的组件树
更新当前组件实例 的组件树 为
新旧组件树 和 ,如果存在 ,即 ,则会命中靶向更新的逻辑,显然我们此时满足条件
注:组件树则指的是该组件对应的 VNode Tree。
小结
总体来看, 指令的实现较为简单,基于数据驱动的理念,当 指令对应的 为 的时候会预先创建一个注释节点在该位置,然后在 发生变化时,命中派发更新的逻辑,对新旧组件树进行 ,从而完成使用 指令元素的动态显示隐藏。

>下面,我们来看一下 指令的实现~
v-show
同样地,对于 指令,我们在 Vue 3 在线模版编译平台输入这样一个栗子:
那么,由它编译生成的 函数:
render(_ctx, _cache, $props, $setup, $data, $options) {
return _withDirectives((_openBlock(), _createBlock("div", null, null, 512 /* NEED_PATCH */)),
[
[_vShow, _ctx.visible]
])
}
此时,这个栗子在 为 时,渲染到页面上的 HTML:

从上面的 函数可以看出,不同于 的三目运算符表达式, 的 函数返回的是 函数的执行。
前面,我们已经简单介绍了 和 函数。那么,除开这两者,接下来我们逐点分析一下这个 函数,首当其冲的是 ~
vShow 在生命周期中改变 display 属性
在源码中则对应着 ,它被定义在 。它的职责是对 指令进行**特殊处理**,主要表现在 、、、 这四个生命周期中:
// packages/runtime-dom/src/directives/vShow.ts
export const vShow: ObjectDirective = {
beforeMount(el, { value }, { transition }) {
el._vod = el.style.display === 'none' ? '' : el.style.display
if (transition && value) {
// 处理 tansition 逻辑
...
} else {
setDisplay(el, value)
}
},
mounted(el, { value }, { transition }) {
if (transition && value) {
// 处理 tansition 逻辑
...
}
},
updated(el, { value, oldValue }, { transition }) {
if (!value === !oldValue) return
if (transition) {
// 处理 tansition 逻辑
...
} else {
setDisplay(el, value)
}
},
beforeUnmount(el, { value }) {
setDisplay(el, value)
}
}
对于 指令会处理两个逻辑:普通 或 时的 情况。通常情况下我们只是使用 指令,命中的就是前者。
这里我们只对普通 情况展开分析。
普通 情况,都是调用的 函数,以及会传入两个变量:
当前使用 指令的真实元素
指令对应的 的值
接着,我们来看一下 函数的定义:
function setDisplay(el: VShowElement, value: unknown): void {
el.style.display = value ? el._vod : 'none'
}
函数正如它本身命名的语意一样,是通过改变该元素的 CSS 属性 的值来动态的控制 绑定的元素的显示或隐藏。
并且,我想大家可能注意到了,当 为 的时候, 是等于的 ,而 则等于这个真实元素的 CSS 属性(默认情况下为空)。所以,当 对应的 为 的时候,元素显示与否是取决于它本身的 CSS 属性。
其实,到这里 指令的本质在源码中的体现已经出来了。但是,仍然会留有一些疑问,例如 做了什么? 在生命周期中对 指令的处理又是如何运用的?
withDirectives 在 VNode 上增加 dir 属性
顾名思义和指令相关,即在 Vue 3 中和指令相关的元素,最后生成的 函数都会调用 处理指令相关的逻辑,**将 的逻辑作为 属性添加**到 上。
函数的定义:
// packages/runtime-core/src/directives.ts
export function withDirectives(
vnode: T,
directives: DirectiveArguments
): T {
const internalInstance = currentRenderingInstance
if (internalInstance === null) {
__DEV__ && warn(`withDirectives can only be used inside render functions.`)
return vnode
}
const instance = internalInstance.proxy
const bindings: DirectiveBinding[] = vnode.dirs || (vnode.dirs = [])
for (let i = 0; i < directives.length; i++) {
let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]
if (isFunction(dir)) {
...
}
bindings.push({
dir,
instance,
value,
oldValue: void 0,
arg,
modifiers
})
}
return vnode
}
首先, 会获取当前渲染实例处理边缘条件,即如果在 函数外面使用 则会抛出异常:
"withDirectives can only be used inside render functions."
然后,在 上绑定 属性,并且遍历传入的 数组,而对于我们这个栗子 就是:
[
[_vShow, _ctx.visible]
]
显然此时只会迭代一次(数组长度为 1)。并且从 传入的 参数可以知道,从 上解构出的 指的是 ,即我们上面介绍的 。由于 是一个对象,所以会重新构造()一个 给 。
的作用体现在 在生命周期改变元素的 CSS 属性,而这些生命周期会作为派发更新的结束回调被调用。
接下来,我们一起来看看其中的调用细节~
派发更新时 patch,注册 postRenderEffect 事件
相信大家应该都知道 Vue 3 提出了 的概念,其用来针对不同的场景来执行对应的 逻辑。那么,对于上面这个栗子,我们会命中 的逻辑。
而对于 之类的指令来说,由于 上绑定了处理元素 CSS 属性的相关逻辑( 定义好的生命周期处理)。所以,此时 中会为注册一个 事件。
// packages/runtime-core/src/renderer.ts
const patchElement = (
n1: VNode,
n2: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
...
// 此时 dirs 是存在的
if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {
// 注册 postRenderEffect 事件
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
}, parentSuspense)
}
...
}
这里我们简单分析一下 和 函数:
, 事件注册是通过 函数完成的,因为 都是维护在一个队列中(为了保持 的有序),这里是 ,所以对于 也是一样的会被进队
,由于 封装了对元素 CSS 属性的处理,所以 的本职是调用指令相关的生命周期处理。并且,需要注意的是此时是**更新逻辑*
,所以 *只会调用 中定义好的 生命周期**
flushJobs 的结束(finally)调用 postRenderEffect
到这里,我们已经围绕 介绍完了 、、 等概念。但是,万事具备只欠东风,还缺少一个**调用 事件的时机**,即处理 队列的时机。
在 Vue 3 中 相当于 Vue 2.x 的 。虽然变了个命名,但是仍然保持着一样的调用方式,都是调用的 函数,然后由 执行 队列。而调用 事件的时机则是在执行队列的结束。
函数的定义:
// packages/runtime-core/src/scheduler.ts
function flushJobs(seen?: CountMap) {
isFlushPending = false
isFlushing = true
if (__DEV__) {
seen = seen || new Map()
}
flushPreFlushCbs(seen)
// 对 effect 进行排序
queue.sort((a, b) => getId(a!) - getId(b!))
try {
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
// 执行渲染 effect
const job = queue[flushIndex]
if (job) {
...
}
}
} finally {
...
// postRenderEffect 事件的执行时机
flushPostFlushCbs(seen)
...
}
}
在 函数中会执行三种 队列,分别是 、、,它们各自对应 、、。
那么,显然 事件的调用时机是在 。而 内部则会遍历 队列,即执行之前在 时注册的 事件,本质上就是执行:
updated(el, { value, oldValue }, { transition }) {
if (!value === !oldValue) return
if (transition) {
...
} else {
// 改变元素的 CSS display 属性
setDisplay(el, value)
}
},
小结
相比较 简单干脆地通过 直接更新元素, 的处理就略显复杂。这里我们重新梳理一下整个过程:
首先,由 来生成最终的 。它会给 上绑定 属性,即 定义的在生命周期中对元素 CSS 属性的处理
其次,在 的阶段,会注册 事件,用于调用 定义的 生命周期处理 CSS 属性的逻辑
最后,在派发更新的结束,调用 事件,即执行 定义的 生命周期,更改元素的 CSS 属性

结语
和 实现的原理,你可以用一两句话概括,也可以用一大堆话概括。如果牵扯到面试场景下,我更欣赏后者,因为这说明你研究的够深以及*
我是五柳,喜欢创新、捣鼓源码,专注于 Vue3 源码、Vite 源码、前端工程化等技术分享,欢迎关注我的微信公众号:Code center。
