前言
又回到了经典的一句话:“知其然,而后使其然”。置信大家对 Vue 提供 v-if
和 v-show
指令的应用以及对应场景应该都滚瓜烂熟了。然而,我想依然会有很多同学对于 v-if
和 v-show
指令实现的原理存在常识空白。
所以,明天就让咱们来一起理解一番 v-if
和 v-show
指令实现的原理~
v-if
在之前 【Vue3 源码解读】从编译过程,了解动态节点晋升 一文中,我给大家介绍了 Vue 3 的编译过程,即一个模版会经验 baseParse
、transform
、generate
这三个过程,最初由 generate
生成能够执行的代码(render
函数)。
这里,咱们就不从编译过程开始解说v-if
指令的render
函数生成过程了,有趣味理解这个过程的同学,能够看我之前的文章从编译过程,了解动态节点晋升
咱们能够间接在 Vue3 Template Explore 输出一个应用 v-if
指令的栗子:
<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
时渲染到页面上会是这样:
<div align="center">
<img width="400" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fa3d336210f34fff8f68d1b8cab83443~tplv-k3u1fbpfcp-zoom-1.image"/>
</div>
在 Vue 中很多中央都使用了正文节点来作为占位节点,其目标是在不展现该元素的时候,标识其在页面中的地位,以便在 patch
的时候将该元素放回该地位。
那么,这个时候我想大家就会抛出一个疑难:当 visible
动静切换 true
或 false
的这个过程(派发更新)到底产生了什么?
派发更新时 patch,更新节点
如果不理解 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
的定义(伪代码):
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
指令元素的动态显示暗藏。
<div align="center">
<img width="700" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fd36f8e0870340eeb0d2fbcf56fec40a~tplv-k3u1fbpfcp-zoom-1.image"/>
</div>
上面,咱们来看一下 v-show
指令的实现~
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] ])}
此时,这个栗子渲染到页面上的 HTML:
<div align="center">
<img width="400" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e16e74b15c044a469baf368c96ddb6ae~tplv-k3u1fbpfcp-zoom-1.image"/>
</div>
从下面的 render
函数能够看出,不同于 v-if
的三目运算符表达式,v-show
的 render
函数返回的是 _withDirectives()
函数的执行。
后面,咱们曾经简略介绍了 _openBlock()
和 _createBlock()
函数。那么,除开这两者,接下来咱们逐点剖析一下这个 render
函数,首当其冲的是 vShow
~
vShow 在生命周期中扭转 display 属性
_vShow
在源码中则对应着 vShow
,它被定义在 packages/runtime-dom/src/directives/vShow
。它的职责是对 v-show
指令进行非凡解决,次要体现在 beforeMount
、mounted
、updated
、beforeUnMount
这四个生命周期中:
// packages/runtime-dom/src/directives/vShowexport 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 在 VNode 上减少 dir 属性
withDirectives()
顾名思义和指令相干,即在 Vue 3 中和指令相干的元素,最初生成的 render
函数都会调用 withDirectives()
解决指令相干的逻辑,将 vShow
的逻辑作为 dir
属性增加到 VNode
上。
withDirectives()
函数的定义:
// packages/runtime-core/directivesexport 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
属性,而这些生命周期会作为派发更新的完结回调被调用。
接下来,咱们一起来看看其中的调用细节~
派发更新时 patch,注册 postRenderEffect
事件
置信大家应该都晓得 Vue 3 提出了 patchFlag
的概念,其用来针对不同的场景来执行对应的 patch
逻辑。那么,对于下面这个栗子,咱们会命中 patchElement
的逻辑。
而对于 v-show
之类的指令来说,因为 Vnode.dir
上绑定了解决元素 CSS display
属性的相干逻辑( vShow
定义好的生命周期解决)。所以,此时 patchElement
中会为注册一个 postRenderEffect
事件。
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
封装了对元素 CSSdisplay
属性的解决,所以invokeDirective
的本职是调用指令相干的生命周期解决。并且,须要留神的是此时是更新逻辑,所以只会调用vShow
中定义好的update
生命周期
flushJobs 的完结(finally)调用 postRenderEffect
到这里,咱们曾经围绕 v-Show
介绍完了 vShow
、withDirectives
、postRenderEffect
等概念。然而,万事具备只欠东风,还短少一个调用 postRenderEffect
事件的机会,即解决 pendingPostFlushCbs
队列的机会.
在 Vue 3 中 effect
相当于 Vue 2.x 的 watch
。尽管变了个命名,然而依然放弃着一样的调用形式,都是调用的 run()
函数,而后由 flushJobs()
执行 effect
队列。而调用 postRenderEffect
事件的机会则是在执行队列的完结。
flushJobs
函数的定义:
// packages/runtime-core/scheduler.tsfunction 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
定义的在生命周期中对元素 CSSdisplay
属性的解决 - 其次,在
patchElement
的阶段,会注册postRenderEffect
事件,用于调用vShow
定义的update
生命周期解决 CSSdisplay
属性的逻辑 - 最初,在派发更新的完结,调用
postRenderEffect
事件,即执行vShow
定义的update
生命周期,更改元素的 CSSdisplay
属性
<div align="center">
<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a2179d0b950e4de7b319372fb87c52b0~tplv-k3u1fbpfcp-zoom-1.image" />
</div>
结语
v-if
和 v-show
原理,你能够用一两句话概括,也能够用一大堆话概括。如果牵扯到面试场景下,我更观赏后者,因为这阐明你钻研的够深以及理解能力够强。并且,当你理解一个指令的处理过程后,对于其余指令 v-on
、v-model
的解决,置信也能够很容易的得出结论。最初,如果文中存在表白不当或谬误的中央,欢送各位同学提 Issue~
我是五柳,喜爱翻新、捣鼓源码,专一于 Vue3 源码、Vite 源码、前端工程化等技术分享,欢送关注我的微信公众号:Code center。