又回到了经典的一句话:“知其然,然后使其然”。相信你们对 Vue 提供 v-if
和 v-show
指令的使用以及对应场景应该都倒背如流了。可是,我想仍然会有不少同窗对于 v-if
和 v-show
指令实现的原理存在知识空白。javascript
因此,今天就让咱们来一块儿了解一番 v-if
和 v-show
指令实现的原理~前端
在以前 【Vue3 源码解读】从编译过程,理解静态节点提高 一文中,我给你们介绍了 Vue 3 的编译过程,即一个模版会经历 baseParse
、transform
、generate
这三个过程,最后由 generate
生成能够执行的代码(render
函数)。vue
这里,咱们就不从编译过程开始讲解
v-if
指令的render
函数生成过程了,有兴趣了解这个过程的同窗,能够看我以前的文章从编译过程,理解静态节点提高java
咱们能够直接在 Vue3 Template Explore 输入一个使用 v-if
指令的栗子:node
<div v-if="visible"></div>
复制代码
而后,由它编译生成的 render
函数会是这样:面试
render(_ctx, _cache, $props, $setup, $data, $options) {
return (_ctx.visible)
? (_openBlock(), _createBlock("div", { key: 0 }))
: _createCommentVNode("v-if", true)
}
复制代码
能够看到,一个简单的使用 v-if
指令的模版编译生成的 render
函数最终会返回一个三目运算表达式。首先,让咱们先来认识一下其中几个变量和函数的意义:前端工程化
_ctx
当前组件实例的上下文,即 this
_openBlock()
和 _createBlock()
用于构造 Block Tree
和 Block VNode
,它们主要用于靶向更新过程_createCommentVNode()
建立注释节点的函数,一般用于占位显然,若是当 visible
为 false
的时候,会在当前模版中建立一个注释节点(也可称为占位节点),反之则建立一个真实节点(即它本身)。例如当 visible
为 false
时渲染到页面上会是这样:数组
在 Vue 中不少地方都运用了注释节点来做为占位节点,其目的是在不展现该元素的时候,标识其在页面中的位置,以便在
patch
的时候将该元素放回该位置。微信
那么,这个时候我想你们就会抛出一个疑问:当 visible
动态切换 true
或 false
的这个过程(派发更新)究竟发生了什么?markdown
若是不了解 Vue 3 派发更新和依赖收集过程的同窗,能够看我以前的文章4k+ 字分析 Vue 3.0 响应式原理(依赖收集和派发更新)
在 Vue 3 中总共有四种指令:v-on
、v-model
、v-show
和 v-if
。可是,实际上在源码中,只针对前面三者进行了特殊处理,这能够在 packages/runtime-dom/src/directives
目录下的文件看出:
// packages/runtime-dom/src/directives
|-- driectives
|-- vModel.ts ## v-model 指令相关
|-- vOn.ts ## v-on 指令相关
|-- vShow.ts ## v-show 指令相关
复制代码
而针对 v-if
指令是直接走派发更新过程时 patch
的逻辑。因为 v-if
指令订阅了 visible
变量,因此当 visible
变化的时候,则会触发派发更新,即 Proxy
对象的 set
逻辑,最后会命中 componentEffect
的逻辑。
固然,咱们也能够称这个过程为组件的更新过程
这里,咱们来看一下 componentEffect
的定义(伪代码):
// 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
)
...
}
}
}
复制代码
能够看到,当组件还没挂载时,即第一次触发派发更新会命中 !instance.isMounted
的逻辑。而对于咱们这个栗子,则会命中 else
的逻辑,即组件更新,主要会作三件事:
nextTree
和以前的组件树 prevTree
instance
的组件树 subTree
为 nextTree
patch
新旧组件树 prevTree
和 nextTree
,若是存在 dynamicChildren
,即 Block Tree
,则会命中靶向更新的逻辑,显然咱们此时知足条件注:组件树则指的是该组件对应的 VNode Tree。
整体来看,v-if
指令的实现较为简单,基于数据驱动的理念,当 v-if
指令对应的 value
为 false
的时候会预先建立一个注释节点在该位置,而后在 value
发生变化时,命中派发更新的逻辑,对新旧组件树进行 patch
,从而完成使用 v-if
指令元素的动态显示隐藏。
下面,咱们来看一下
v-show
指令的实现~
一样地,对于 v-show
指令,咱们在 Vue 3 在线模版编译平台输入这样一个栗子:
<div v-show="visible"></div>
复制代码
那么,由它编译生成的 render
函数:
render(_ctx, _cache, $props, $setup, $data, $options) {
return _withDirectives((_openBlock(), _createBlock("div", null, null, 512 /* NEED_PATCH */)),
[
[_vShow, _ctx.visible]
])
}
复制代码
此时,这个栗子在 visible
为 false
时,渲染到页面上的 HTML:
从上面的 render
函数能够看出,不一样于 v-if
的三目运算符表达式,v-show
的 render
函数返回的是 _withDirectives()
函数的执行。
前面,咱们已经简单介绍了 _openBlock()
和 _createBlock()
函数。那么,除开这二者,接下来咱们逐点分析一下这个 render
函数,首当其冲的是 _vShow
~
_vShow
在源码中则对应着 vShow
,它被定义在 packages/runtime-dom/src/directives/vShow
。它的职责是对 v-show
指令进行特殊处理,主要表如今 beforeMount
、mounted
、updated
、beforeUnMount
这四个生命周期中:
// packages/runtime-dom/src/directives/vShow.ts
export const vShow: ObjectDirective<VShowElement> = {
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)
}
}
复制代码
对于 v-show
指令会处理两个逻辑:普通 v-show
或 transition
时的 v-show
状况。一般状况下咱们只是使用 v-show
指令,命中的就是前者。
这里咱们只对普通
v-show
状况展开分析。
普通 v-show
状况,都是调用的 setDisplay()
函数,以及会传入两个变量:
el
当前使用 v-show
指令的真实元素v-show
指令对应的 value
的值接着,咱们来看一下 setDisplay()
函数的定义:
function setDisplay(el: VShowElement, value: unknown): void {
el.style.display = value ? el._vod : 'none'
}
复制代码
setDisplay()
函数正如它自己命名的语意同样,是经过改变该元素的 CSS 属性 display
的值来动态的控制 v-show
绑定的元素的显示或隐藏。
而且,我想你们可能注意到了,当 value
为 true
的时候,display
是等于的 el.vod
,而 el.vod
则等于这个真实元素的 CSS display
属性(默认状况下为空)。因此,当 v-show
对应的 value
为 true
的时候,元素显示与否是取决于它自己的 CSS display
属性。
其实,到这里
v-show
指令的本质在源码中的体现已经出来了。可是,仍然会留有一些疑问,例如withDirectives
作了什么?vShow
在生命周期中对v-show
指令的处理又是如何运用的?
withDirectives()
顾名思义和指令相关,即在 Vue 3 中和指令相关的元素,最后生成的 render
函数都会调用 withDirectives()
处理指令相关的逻辑,将 vShow
的逻辑做为 dir
属性添加到 VNode
上。
withDirectives()
函数的定义:
// packages/runtime-core/src/directives.ts
export function withDirectives<T extends VNode>( 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()
会获取当前渲染实例处理边缘条件,即若是在 render
函数外面使用 withDirectives()
则会抛出异常:
"withDirectives can only be used inside render functions."
而后,在 vnode
上绑定 dirs
属性,而且遍历传入的 directives
数组,而对于咱们这个栗子 directives
就是:
[
[_vShow, _ctx.visible]
]
复制代码
显然此时只会迭代一次(数组长度为 1)。而且从 render
传入的 参数能够知道,从 directives
上解构出的 dir
指的是 _vShow
,即咱们上面介绍的 vShow
。因为 vShow
是一个对象,因此会从新构造(bindings.push()
)一个 dir
给 VNode.dir
。
VNode.dir
的做用体如今 vShow
在生命周期改变元素的 CSS display
属性,而这些生命周期会做为派发更新的结束回调被调用。
接下来,咱们一块儿来看看其中的调用细节~
postRenderEffect
事件相信你们应该都知道 Vue 3 提出了 patchFlag
的概念,其用来针对不一样的场景来执行对应的 patch
逻辑。那么,对于上面这个栗子,咱们会命中 patchElement
的逻辑。
而对于 v-show
之类的指令来讲,因为 Vnode.dir
上绑定了处理元素 CSS display
属性的相关逻辑( vShow
定义好的生命周期处理)。因此,此时 patchElement()
中会为注册一个 postRenderEffect
事件。
// 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)
}
...
}
复制代码
这里咱们简单分析一下 queuePostRenderEffect()
和 invokeDirectiveHook()
函数:
queuePostRenderEffect()
,postRenderEffect
事件注册是经过 queuePostRenderEffect()
函数完成的,由于 effect
都是维护在一个队列中(为了保持 effect
的有序),这里是 pendingPostFlushCbs
,因此对于 postRenderEffect
也是同样的会被进队
invokeDirectiveHook()
,因为 vShow
封装了对元素 CSS display
属性的处理,因此 invokeDirective()
的本职是调用指令相关的生命周期处理。而且,须要注意的是此时是更新逻辑,因此只会调用 vShow
中定义好的 update
生命周期
postRenderEffect
到这里,咱们已经围绕 v-Show
介绍完了 vShow
、withDirectives
、postRenderEffect
等概念。可是,万事具有只欠东风,还缺乏一个调用 postRenderEffect
事件的时机,即处理 pendingPostFlushCbs
队列的时机。
在 Vue 3 中 effect
至关于 Vue 2.x 的 watch
。虽然变了个命名,可是仍然保持着同样的调用方式,都是调用的 run()
函数,而后由 flushJobs()
执行 effect
队列。而调用 postRenderEffect
事件的时机则是在执行队列的结束。
flushJobs()
函数的定义:
// 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)
...
}
}
复制代码
在 flushJobs()
函数中会执行三种 effect
队列,分别是 preRenderEffect
、renderEffect
、postRenderEffect
,它们各自对应 flushPreFlushCbs()
、queue
、flushPostFlushCbs
。
那么,显然 postRenderEffect
事件的调用时机是在 flushPostFlushCbs()
。而 flushPostFlushCbs()
内部则会遍历 pendingPostFlushCbs
队列,即执行以前在 patchElement
时注册的 postRenderEffect
事件,本质上就是执行:
updated(el, { value, oldValue }, { transition }) {
if (!value === !oldValue) return
if (transition) {
...
} else {
// 改变元素的 CSS display 属性
setDisplay(el, value)
}
},
复制代码
相比较 v-if
简单干脆地经过 patch
直接更新元素,v-show
的处理就略显复杂。这里咱们从新梳理一下整个过程:
widthDirectives
来生成最终的 VNode
。它会给 VNode
上绑定 dir
属性,即 vShow
定义的在生命周期中对元素 CSS display
属性的处理patchElement
的阶段,会注册 postRenderEffect
事件,用于调用 vShow
定义的 update
生命周期处理 CSS display
属性的逻辑postRenderEffect
事件,即执行 vShow
定义的 update
生命周期,更改元素的 CSS display
属性v-if
和 v-show
实现的原理,你能够用一两句话归纳,也能够用一大堆话归纳。若是牵扯到面试场景下,我更欣赏后者,由于这说明你研究的够深以及理解能力够强。而且,当你了解一个指令的处理过程后,对于其余指令 v-on
、v-model
的处理,相信也能够很容易地得出结论。最后,若是文中存在表达不当或错误的地方,欢迎各位同窗提 Issue~
我是五柳,喜欢创新、捣鼓源码,专一于 Vue3 源码、Vite 源码、前端工程化等技术分享,欢迎关注个人微信公众号:Code center。