Skip to content

vue-directive-and-effect-scope

自動解除的魔法呢?Vue Directive 和 Effect Scope

Vue 除了元件與 Composition API 外,還有很方便的自定義 Directive

前陣子的新作 - 彈珠檯,就用了自定義指令,可以將 DOM 變成彈珠檯的中的機關,像這樣:

vue
<template>
  <div
    v-pinball-mech
    class="wall absolute bottom-0 left-0 h-4 w-full rounded-full"
  />
</template>

其中有個需要取得 DOM 尺寸與位置的邏輯,這裡取簡化後的核心程式碼:

ts
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 同步尺寸變化。

不過當元件移除時,指令中 watchuseElementBounding 並不會自動解除呦。( ゚ ∀。)

真的會這樣嗎?

要證明這件事,其實也相當簡單,來個 useIntervalFn

正常情況下 useIntervalFn 會在 Effect Scope 停止時自動解除。

ts
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 即可:

ts
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 是否依然存在,不存在則解除。

ts
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,並在適當時機停止。