自動解除的魔法呢?Vue Directive 和 Effect Scope
Vue 除了元件與 Composition API 外,還有很方便的自定義 Directive。
前陣子的新作 - 彈珠檯,就用了自定義指令,可以將 DOM 變成彈珠檯的中的機關,像這樣:
<template>
<div
v-pinball-mech
class="wall absolute bottom-0 left-0 h-4 w-full rounded-full"
/>
</template>
其中有個需要取得 DOM 尺寸與位置的邏輯,這裡取簡化後的核心程式碼:
export const vPinballMech: Directive = {
mounted(el) {
if (!(el instanceof HTMLElement)) {
console.warn('v-pinball-mech 只能用於 HTMLElement')
return
}
const bounding = reactive(useElementBounding(el))
const id = store.add({
options,
...bounding,
})
watch(bounding, (value) => {
store.update(id, value)
}, { deep: true })
},
}
當元素綁定後,在 store
新增此元素,配合 useElementBounding
取得元素尺寸並用 watch
同步尺寸變化。
不過當元件移除時,指令中 watch
與 useElementBounding
並不會自動解除呦。( ゚ ∀。)
真的會這樣嗎?
要證明這件事,其實也相當簡單,來個 useIntervalFn
:
正常情況下 useIntervalFn
會在 Effect Scope 停止時自動解除。
export const vPinballMech: Directive = {
mounted(el, binding) {
useIntervalFn(() => {
console.log('cod')
}, 1000)
},
}
可以注意到即使元件被移除,cod
仍然會不斷出現。
為甚麼會這個樣子呢?這個我們可以反過來想,Vue 元件移除時如何清理響應式資料與副作用呢?
相關概念其實曾經在這篇文章提過。
主要是 Vue 會在元件建立自動產生元件範圍的 Effect Scope 並在移除時自動停止 Effect Scope。
而 Directive 並沒有自己的 Effect Scope,所以裡面的響應式資料不會被自動清理。
所以怎麼知道 Directive 沒有 Effect Scope ?很簡單,使用 getCurrentScope
即可:
export const vPinballMech: Directive = {
mounted(el) {
if (!(el instanceof HTMLElement)) {
console.warn('v-pinball-mech 只能用於 HTMLElement')
return
}
const currentScope = getCurrentScope()
console.log('currentScope: ', currentScope)
// ...
},
}
會注意到 console 結果是 currentScope: undefined
。
你說為甚麼 Directive 內可以取得來源元件,卻沒有 Effect Scope?(´・ω・`)
具體原因我也不清楚,就看有沒有大大知道為甚麼了。
所以該怎麼辦呢?
解決方法其實很簡單,就是自己建一個 Effect Scope,然後自己的 Scope 自己關。ლ(´∀`ლ)
不過不知道為甚麼在 mounted
之外的地方呼叫 scope.stop()
都沒有效(無效的程式碼在這裡)
所以改成在 mounted
中持續判斷 DOM 是否依然存在,不存在則解除。
export const vPinballMech: Directive = {
mounted(el) {
if (!(el instanceof HTMLElement)) {
console.warn('v-pinball-mech 只能用於 HTMLElement')
return
}
const scope = effectScope()
scope.run(() => {
const bounding = reactive(useElementBounding(el))
const id = store.add({
options,
...bounding,
})
watch(bounding, (value) => {
store.update(id, value)
}, { deep: true })
useIntervalFn(() => {
// 檢查元素是否還在 DOM 中,否則停止 effect scope
if (!el.isConnected) {
scope.stop()
}
}, 100)
})
},
}
現在在元件被移除後,響應式資料與副作用都會自動解除了。✧⁑。٩(ˊᗜˋ*)و✧⁕。
總結 🐟
- Vue Directive 自身沒有 Effect Scope,所以裡面的響應式資料與副作用不會被自動清理。
- 可自行建立 Effect Scope,並在適當時機停止。