---
url: /blog-vue/vue-3-5-update-on-watcher-cleanup-with-the-forgotten-on-cleanup.md
description: >-
  Vue 3.5 更新新增了 onWatcherCleanup，用於清理 watcher 中的副作用（Side Effect），可是原本的 watch 已經有
  onCleanup，所以到底差在哪呢？(╯▽╰ )
---

![vue-3-5-update-on-watcher-cleanup-with-the-forgotten-on-cleanup](/vue-3-5-update-on-watcher-cleanup-with-the-forgotten-on-cleanup.webp){.cover}

# Vue 3.5 更新，onWatcherCleanup 與 onCleanup

[Vue 3.5 更新了](https://blog.vuejs.org/posts/vue-3-5)！除了其他改進與實用功能外，有一個很有趣的新 API，[叫做 `onWatcherCleanup`](https://blog.vuejs.org/posts/vue-3-5#onwatchercleanup)。

不過原本的 watch 已經有 `onCleanup` 了，所以到底差在哪呢？

[文件 Side Effect Cleanup 一節](https://vuejs.org/guide/essentials/watchers.html#side-effect-cleanup)中提到的用法為：

```ts
import { onWatcherCleanup, watch } from 'vue'

watch(id, (newId) => {
  const controller = new AbortController()

  fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
    // callback logic
  })

  onWatcherCleanup(() => {
    // abort stale request
    controller.abort()
  })
})
```

不過 watch 本身就有 onCleanup，所以應該可以這樣寫。

```ts
watch(id, (newId, oldId, onCleanup) => {
  const controller = new AbortController()

  fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
    // callback logic
  })

  onCleanup(() => {
    // abort stale request
    controller.abort()
  })
})
```

[文件 onWatcherCleanup 章節](https://vuejs.org/api/reactivity-core#onwatchercleanup)也沒有其他說明。

說不定實際效果不一樣？讓我們來實測看看吧。%(๑•̀ㅂ•́)و✧%

這裡我們使用一個[免費的 API](https://alexwohlbruck.github.io/cat-facts/docs/) 來測試，順便取得貓貓冷知識 XD。

::: warning
如果 API 壞了，大家可以換成[其他 API 測試](https://free-apis.github.io/#/browse)。
:::

## 實際測試

```vue
<script setup>
import { onWatcherCleanup, ref, watch } from 'vue'

const id = ref('cod')
const data = ref([])

watch(id, (newId, oldId, onCleanup) => {
  const controller = new AbortController()

  fetch(
    `https://cat-fact.herokuapp.com/facts?id=${newId}`,
    { signal: controller.signal }
  )
    .then((res) => res.json())
    .then((value) => {
      data.value = value
    })
    .catch((error) => {
      console.error(error)
    })

  onWatcherCleanup(() => {
    console.log('[ onWatcherCleanup ] : ', controller)
    controller.abort()
  }) // */

  /* onCleanup(() => {
    console.log('[ onCleanup ] : ', controller);
    controller.abort()
  }) // */
})

function getData() {
  id.value = crypto.randomUUID()
}
</script>

<template>
  <button @click="getData()">
    取得貓貓小知識
  </button>

  <p v-for="datum, i in data" :key="i">
    {{ i + 1 }}. {{ datum.text }}
  </p>
</template>
```

[範例程式碼](https://play.vuejs.org/#eNqdVM1u1DAQfpWRhbRpSRNVhcuyWyhtD+UACKg41BV1E2fXrWNHtrMtWuUZekACbpw4IiEegLdZ0cdg7OxPCqhIXa1ie2a+b2Y+TzIlO1WVTGpO+mRgMyMqB5a7utqmSpSVNg6mYHgRwwVz2TgGrd76DTe7kjNVV9BAYXQJPeToUUVVppV1IHIYelzUy3TeW1uYc+bY3HF0jFaqAm0k8hgixS8OcNUyD4uaZ1iD4TZMqQJoOfDpjJaSG2RCDOycYpm7S2sUeAEK7pn9DuBk7Fxl+2maMbdRsMwl2IE+rxk2n+ky9Sb7WOTDe9NQRXMSt8ApWDFSTPY7aZPWBI0PwWQ+LnFjrqLIcBvKxTU5s1pFazf9EyZrvmrI/7wkSbBjO2F91LqaBRRrxkYibow2N7FeEC15ElzzgA7a75YyRlEXu0BKPYp6R6soOIY+9OJOtwvCTv/MC+5l9mkgTWE9bZOl63/Nx3/S/jFMd87etlvUKnNCKxhxt4e6Yu6QWORLhTPzvnI6MUzlujw8PNiLfAq8ykHajj8OPh4cLyvJHMcTwOC0dg5Zn2RSZOdDSpb0lGzPrj7Ofn66/vEB/7PvV7++fL3+9nmQtpBAhgQVTDYKbRCK112XMQgQKlw9MswnbYrG+7AJTZP4QwhMHL90aAkkqX8pB2mnNBITZ1GaQozCuOFLHPqlBIe6EqjWi8rrYSnpL66AEialvngWbM7UfD7piBnz7Pwf9jN76W2UvMSx5mbCKVn6HDMoRuvef/0cq+04S53XEqNvcb7iOA+1r7ENe1qrHMvuxIVqD8KnSKjRG7t/6biyi6Z8oWESQjwl+BXavaX1VblbyYOAw7tHFd9NuPGcKOBW8jDZJM1vjAKyGQ==)

就一個簡單的按鈕，點擊後會取得貓貓小知識，並顯示在畫面上。

![測試程式](/vue-3-5-update-on-watcher-cleanup-with-the-forgotten-on-cleanup/1.webp)

每次 watch 觸發時，會呼叫上一次的 onCleanup 或 onWatcherCleanup，這樣就可以清理上一次的副作用。

所以讓我們快速點擊按鈕兩下，觀察一下 DevTools 之 Network 的結果。

![Network](/vue-3-5-update-on-watcher-cleanup-with-the-forgotten-on-cleanup/2.webp)

可以注意到上次的 fetch 順利取消了！

現在讓我們把 onWatcherCleanup 註解掉，改用 onCleanup，再來觀察一下。

見證奇蹟的時刻到了！

結果一模一樣！%╮(╯▽╰)╭%

...嗯，以上例子的確看不出差別，但是不代表 onWatcherCleanup 沒有用途，讓我們再來看看其他情境吧。

## onWatcherCleanup 使用情境

::: tip
感謝 Line Vue.js Taiwan 社群裡的 dog 大大[提供的資訊](https://github.com/vuejs/core/pull/9927)，忽然茅舍頓開。%ᕕ( ﾟ ∀。)ᕗ%
:::

目前來看 onWatcherCleanup 比較類似 getCurrentScope、onScopeDispose 這類底層 API，平常不會特別使用，一般業務邏輯用 onCleanup 就好。（想統一用 onWatcherCleanup 也是可以 XD）

不過如果你希望「在 watch 中重新執行時，會自動清理上一次動作」的實用 API，這時候 onWatcherCleanup 就派上用場了。

概念上有點類似 [VueUse 的 useEventListener](https://vueuse.org/core/useEventListener/#useeventlistener)，此 API 可以在元件中註冊事件，並在元件被銷毀時自動移除。

```ts
import { useEventListener } from '@vueuse/core'

// 像這樣註冊事件，不用手動 removeEventListener
useEventListener(document, 'visibilitychange', (evt) => {
  console.log(evt)
})
```

只是 useEventListener 的目標是「整個元件的 effect scope」，而 onWatcherCleanup 的目標是「單一 watcher scope」。

讓我們延伸剛剛的例子，設計一個在 watch 使用時會自動取消前一次請求，並在元件解除後也會自動取消請求的 API。

新增一個 useCatFact。

```js
import { onBeforeUnmount, onWatcherCleanup, shallowRef } from 'vue'

export function useCatFact() {
  const abortController = shallowRef()

  /** 元件解除前，自動取消請求 */
  onBeforeUnmount(() => {
    abortController.value?.abort()
  })

  /** 取得貓貓小知識 */
  function getFact(id) {
    return new Promise((resolve, reject) => {
      const controller = new AbortController()

      fetch(
        `https://cat-fact.herokuapp.com/facts?id=${id}`,
        { signal: controller.signal }
      )
        .then((res) => res.json())
        .then((value) => resolve(value))
        .catch((error) => {
          // 忽略終止錯誤
          if (error.name === 'AbortError')
            return
          reject(error)
        })

      /**
       * 在 watcher 範圍內會自動取消前一次請求
       *
       * 第二個參數 true 表示忽略不在 watcher 範圍內警告
       */
      onWatcherCleanup(() => {
        controller.abort()
      }, true)

      abortController.value = controller
    })
  }

  return {
    getFact,
  }
}
```

接著在元件中使用。

```vue
<script setup>
import { ref, watch } from 'vue'
import { useCatFact } from './useCatFact.js'

const id = ref('cod')
const data = ref([])

const { getFact } = useCatFact()

watch(id, async (newId) => {
  data.value = await getFact(newId)
})

async function getData() {
  id.value = crypto.randomUUID()
}
</script>

<template>
  <button @click="getData()">
    取得貓貓小知識
  </button>

  <p v-for="datum, i in data" :key="i" class="tip">
    {{ i + 1 }}. {{ datum.text }}
  </p>
</template>
```

現在連續點擊按鈕，可以看到前次請求都會取消，而且元件解除時也會自動取消請求。

[範例程式碼](https://play.vuejs.org/#eNp9VW1rG0cQ/ivDUdDJVe4Iab+4ktPEccH90Ia0ph96pbncray1T7vH7p7sIAQhbuiLbUwp7gsYSlqnCalN2kKDSd/+jE9SPvUvZHbvdLpTRIRf2NmZ2WeeeWbUt67EsdNLiLVoNWUgaKxAEpXESx6j3ZgLBX0QpN2ALV8FHRhAW/Au1DCiVvJIJFn21Tt+oAoPx50anQ1Ze8tjHgs4kwpoCC2d1K4FPKzVJ+bQV35+8fEnaJ3Y+7BOJrlbpafsuslpgNk0bIAvb7MAbEa2VsM6tJag7zEwaZ2eHyUEo/0tn6pJvtwTswzMc1l8O2GBopxpr2sYa9ezPDQssgTidqy4I3wW8u7a2uo1A2XgsaabcYjs4UGRbhz5iuAJoHkrUQqzvh1ENNhseVaR3rOW0oNv03+/G//xDf6kvx2MfnwwPv2+6WYhJhkmiCGIfCkxVNHYs6B3oc0FnrDApNsACpSZYjGf9gfo99H4OlyEwcDRB+PoKLKNTCJYTOnqPjfdElCrYSmJvLfpOnaNM9SFqd6zAt6NaUTE+7FmR3rWYsaLvvOjiG+9a2xKJKQxsQcdEmzOsW/IbW3zrOuCSCJ6xLOKO+ULpCa7XvngPURbuuzyMInQ+xWXN4jkUaIxZm5XExYi7JKfQbtqtEvZ+odyZVsRJidFaaDac2D8PQulvvyK0qdwLzlvmDhUArJYUT+yWMwKZx9pxRKxHBGfJXEDZMcguqHHjLOrBNtK1liXJ2w6TmbgBCoBf8m2SVUItTwSCAw9ALLR8W+h4zJnSvAI8aN0p29pzeYJAdyFBUjv7Zz/9XT88OfnPxynX+7///fe+PPH6e4hinP49Ivxr7vD3+/CgpsFzOC08WUzcOYSZh/OJueyY8yoeOM1eAnAvCkoXizPpamV4pAXDwpcWoIBTjRcR8KoJLaN2uJRjzTwcoMEqopwQhH+nbKjw69UoWdop1FtotfN9Axws6NULBddN/DVhbZuODaXbyY+blYcGleb5GUatl7r03Bws1GO7YOk68yPFks4nMyEQiocc8ayj6M6hJnqTEX430yqXZ/nZYif+Gk2ckvVF5FjUTYRgotZmvTHdSH975/R4YPRn3eHpz8933syfnxcdaFtyOId5ndxSbZaUDNUrmhjrZ53yLR8GpV1Jn+4cocrucK7FkjpHtVy9Cj7UsLOjZ58lh7tp/ceDo92yqJFGZ+f3Rme3M/UW4mvHEYnJ+fP9tI7u+nBzvDwzCwBGN9/NDp+lhV+frY/98Hx6S/p11+Vc+Vy1Z/ZUZ+dklyFk75XpsNw0DBAZpiYO1v6W6kwFURmJOqdVGTIB6XAkE9TrkrjqNfXpz0i9ErEzXXJedO5aA1eAOJa7Q0=)

::: tip
這個時候可能有人會有疑問：「如果把 `onCleanup` 傳到 `useCatFact` 裡，不也可以嗎？」

的確可以，就看設計需求與風格了，像是剛剛提到的 `useEventListener` 就是自動處理的方式。%( •̀ ω •́ )✧%
:::

可以看到程式碼變得相當乾淨，不需要手動 onCleanup，也不用擔心忘記取消請求，非常方便。%∠( ᐛ 」∠)\_%

除了發送 HTTP 請求，這在設計動畫也十分實用，可以有效防止同一物件上的動畫重複執行。

舉一個動畫的例子，這裡使用 [anime.js](https://animejs.com/)。

設計一個名為 useBoxAnimation 的動畫工具，可以對指定元素執行各類預先定義好的動畫。

```ts
import anime from 'animejs'

export function useBoxAnimation() {
  /** 動畫目標元素 */
  const boxRef = ref<HTMLElement>()

  /** 元件解除前，刪除動畫 */
  onBeforeUnmount(() => {
    anime.remove(boxRef.value)
  })

  function translate(x, y) {
    anime({
      targets: boxRef.value,
      translateX: x,
      translateY: y,
      easing: 'spring(1, 80, 10, 0)'
    })

    onWatcherCleanup(() => {
      anime.remove(boxRef.value)
    }, true)
  }

  function rotate(deg) {
    anime({
      targets: boxRef.value,
      rotate: deg
    })

    onWatcherCleanup(() => {
      anime.remove(boxRef.value)
    }, true)
  }

  return {
    boxRef,
    translate,
    rotate,
  }
}
```

以上只是我目前的心得與理解，如果有更多的使用情境或是更深入的解釋，歡迎大家一起討論！%(´▽\`ʃ♡ƪ)%

## 總結 🐟

* 認識 `onCleanup` 與 `onWatcherCleanup`
* `onWatcherCleanup` 適合用在封裝後的功能，一般業務邏輯用 `onCleanup` 就好
* 官方文件的 `onWatcherCleanup` 例子舉的不太好，看不出與 `onCleanup` 的應用差異，害我一開始看得一頭霧水 XD

最後再次感謝 dog 大大提供的資訊，也歡迎大家加入 [Vue.js 社群](https://web-tech-tw.github.io/openchat/#/join/vue-tw)

一起討論 Vue 相關議題！(/≧▽≦)/
