又回到了經典的一句話:「知其然,然後使其然」。相信你們對 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。