---
url: /blog-vue/vue-directive-and-effect-scope.md
description: Directive 內沒有自動解除響應式資料與副作用的魔法呦 (・∀・)
---

![vue-directive-and-effect-scope](/vue-directive-and-effect-scope.webp){.cover}

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

Vue 除了元件與 Composition API 外，還有很方便的自定義 [Directive](https://cn.vuejs.org/guide/reusability/custom-directives)。

前陣子的新作 - [彈珠檯](https://chillcomponent.codlin.me/components/util-pinball/)，就用了自定義指令，可以將 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` 同步尺寸變化。

不過當元件移除時，指令中 `watch` 與 `useElementBounding` 並不會自動解除呦。%( ﾟ ∀。)%

## 真的會這樣嗎？

要證明這件事，其實也相當簡單，來個 [`useIntervalFn`](https://vueuse.org/shared/useIntervalFn/)：

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

```ts
export const vPinballMech: Directive = {
  mounted(el, binding) {
    useIntervalFn(() => {
      console.log('cod')
    }, 1000)
  },
}
```

可以注意到即使元件被移除，`cod` 仍然會不斷出現。

為甚麼會這個樣子呢？這個我們可以反過來想，Vue 元件移除時如何清理響應式資料與副作用呢？

相關概念其實曾經在[這篇文章](/blog-vue/vue-watch-may-cause-a-memory-leak)提過。

主要是 Vue 會在元件建立自動產生元件範圍的 [Effect Scope](https://cn.vuejs.org/api/reactivity-advanced#effectscope) 並在移除時自動停止 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()` 都沒有效（無效的程式碼在[這裡](https://gitlab.com/side_project/chill-component/-/blob/develop/src/components/util-pinball/v-pinball-mech.ts?ref_type=heads)）

所以改成在 `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，並在適當時機停止。
