iT邦幫忙

2025 iThome 鐵人賽

DAY 9
0
Vue.js

Vue3.6 的革新:深入理解 Composition API系列 第 9

Day 9: watchEffect 的介紹

  • 分享至 

  • xImage
  •  

Vue 3 提供的 watchEffect 是一個專門用來處理「副作用 (Side Effect)」的響應式 API。

它可以 自動追蹤依賴,在資料變更時重新執行函式,避免手動指定監控來源,特別適合需要快速聲明式副作用的場景,例如:DOM 更新、API 呼叫、資料同步、除錯 log。

今天讓我們一起瞧瞧 watchEffect 的底層邏輯和核心概念吧!

底層邏輯


watchEffect 是 Vue 3 中一個非常重要的響應式 API,主要用於設定副作用函式。其底層邏輯是透過偵測依賴的變化來自動執行函式,也就是當任何一個被引用的響應式資料改變時,函式都會自動重新執行。

  • 依賴追蹤:
    watchEffect 在執行時會自動追蹤其所依賴的所有響應式資料,不用像 watch 那樣手動指定來源,函式中用到的任何 ref / reactive 都會被自動追蹤。

  • 副作用函式:
    watchEffect 的回撥函式不需要明確的返回值,這與計算屬性 computed 的差異在於, computed 主要是為了計算出某個值,而 watchEffect 主要是用於觸發副作用。

  • 自動清理:
    當依賴的資料變更時, computed 也會自動清理上一次執行的副作用,這對於避免記憶體洩漏和副作用的堆疊非常有用。

特點:

  • 只要依賴的 ref / reactive 值有變化,就會重新執行。
  • 初始執行一次。
  • 不用手動指定監控來源,系統會自動追蹤。
<script lang="ts" setup>
import { ref, watchEffect } from 'vue'

const count = ref(0)

watchEffect(() => {
  console.log('count is', count.value)
})
</script>

watchEffect 使用方法


基本使用

import { reactive, watchEffect, WatchStopHandle } from 'vue';

interface State {
  count: number;
}

const state = reactive<State>({ count: 0 });

/**
 * watchEffect 會立刻執行一次,並自動追蹤 effect 內讀取到的 reactive/ref。
 * 回傳值是一個停止函式(WatchStopHandle)。
 */
const stop: WatchStopHandle = watchEffect((onInvalidate) => {
  // 每次 state.count 被讀取就會被追蹤,當 count 改變時 effect 會重新執行
  console.log(`Count: ${state.count}`);

  // 如果 effect 裡面有非同步工作或 timer,要在 onInvalidate 裡做清理
  const timer = setTimeout(() => {
    console.log('(示範)延遲處理:', state.count);
  }, 300);

  // 當這個 effect 被重新執行或被 stop() 停止時,onInvalidate 的 callback 會被呼叫
  onInvalidate(() => {
    clearTimeout(timer);
    // 也可做其他清理,例如取消 fetch 的 AbortController
  });
});

state.count++;  // 改變值會觸發上面那段 effect

stop();  // 如果不想再監聽,呼叫 stop()

使用選項

watchEffect 還支援一些選項引數,例如 flush 來控制執行時機,以及 onTrackonTrigger 用於響應式追蹤的回撥。

<script lang="ts" setup>
import { ref, watchEffect } from 'vue'

const count = ref(0)

watchEffect((onCleanup) => {
  const timer = setInterval(() => count.value++, 1000)

  onCleanup(() => clearInterval(timer))  // 清理副作用,避免重複計時器
})
</script>

watchEffectwatch 的區別

一樣都是追蹤,那 watchwatchEffect 有什麼不同呢?
讓我們依照不同特性來觀察 watchwatchEffect 的差別:

特性 watch watchEffect
依賴收集 手動指定 自動收集
適合場景 複雜邏輯,需要 new/old 值 快速聲明副作用
清理方式 回呼內自行處理 內建 onCleanup
執行時機 初始不執行(可設定) 初始就執行一次

watch 需要 明確指定 依賴,適合需求較為複雜的場景。
watchEffect 自動追蹤 依賴,適合邏輯相對簡單且依賴強相關的情況。

watchEffect 副作用 (Side Effect)


今天一開頭就提到 watchEffect 是用來處理副作用,那...為什麼會有副作用?副作用會影響什麼呢?

讓我們繼續看下去...

為什麼會有副作用 (Side Effect)

watchEffect 本身設計就是「用來執行副作用」。

在程式設計裡,副作用指的是:
任何會影響「函數外部環境」的行為(例如:修改 DOM、發送請求、寫入 localStorage...)。

我們用以下兩個例子觀察副作用
純函數(Pure Function):

function double(x) {
  return x * 2  // 不改變外部狀態
}

副作用函數:

function updateDom(text) {
  document.body.innerText = text  // 修改了外部的 DOM
}

watchEffect 就是專門用來做「響應式 → 副作用」的橋樑。

常見副作用情境

以下是 watchEffect 中容易遇到的副作用:

無限循環 (Infinite Loop)
⚠️ 問題:如果在 watchEffect 內部 修改了它自己監聽的依賴值,就會觸發無限重跑。
從以下程式碼可以看到無限循環的卡死狀態,因為 count 一直被改 → 又觸發 watchEffect

const count = ref(0)

watchEffect(() => {
  // 錯誤:內部改動自己監聽的值
  count.value++
})

解法:如果只是單純要呼叫累加,不如可以改用方法控制

const count = ref(0)

function increment() {
  count.value++
}

多次觸發 / 過度請求
⚠️ 問題:當 reactive 值頻繁變化(例如輸入框綁定),會導致 watchEffect 過度觸發,可能造成 API 連續請求。

const keyword = ref('')

watchEffect(() => {
  fetch(`/api/search?q=${keyword.value}`) // 每次輸入字母就觸發
})

解法:加上 防抖 (debounce) 或改用 watch 搭配 flush: 'post' 控制觸發時機。

  • watchEffect 裡加防抖 (debounce):這裡引用 lodash.debounce

    import { ref, watchEffect } from 'vue'
    import debounce from 'lodash.debounce'
    
    const keyword = ref('')
    
    
    /**
     * 包裝 API 請求,加上防抖
     * 就算使用者手速再快,實際請求也會延遲 500ms,並且只發送最後一次
     */
    const searchApi = debounce((q: string) => {
      fetch(`/api/search?q=${q}`)
        .then(res => res.json())
        .then(data => console.log('result:', data))
    }, 500)
    
    watchEffect(() => {
      // 當 keyword 改變時觸發,但實際呼叫被 debounce 控制
      searchApi(keyword.value)
    })
    
  • 改用 watch 搭配 flush: 'post' 控制觸發時機

    import { ref, watch } from 'vue'
    
    const keyword = ref('')
    
    watch(
      keyword,
      async (newVal) => {
        // 這裡是 API 請求
        const res = await fetch(`/api/search?q=${newVal}`)
        const data = await res.json()
        console.log('result:', data)
      },
      {
        flush: 'post', // 保證 DOM 更新完成後再執行
        immediate: false, // 預設不會立刻執行,等到 keyword 改變才跑
      }
    )
    

    在 Vue 3 的 watch / watchEffect 裡,有個 option 叫做 flush,用來決定副作用 callback 觸發的時機,目前共有 3 個選項:

    1. pre → 搶在渲染前先跑。
    2. sync → 反應最快,但 DOM 還是舊的。
    3. post → 等畫面更新完再跑,最適合需要讀取 DOM 的情境。
      flush

資源洩漏 (Resource Leak)
⚠️ 問題:如果在 watchEffect 內綁定事件 / 啟動計時器,但沒有清理,會導致資源越堆越多。

watchEffect(() => {
  window.addEventListener('resize', () => {
    console.log('resized!')
  })
})

解法:利用清理函數 (cleanup)。

watchEffect((onCleanup) => {
  const handler = () => console.log('resized!')
  window.addEventListener('resize', handler)

  // 依賴變化或 effect 停止時,自動清理
  onCleanup(() => {
    window.removeEventListener('resize', handler)
  })
})

觸發時機問題
⚠️ 問題:watchEffect 預設是同步執行(DOM 尚未更新),所以有時候拿到的 DOM 狀態不是最新的。
解法:使用 flush: 'post' 改成後置執行

watchEffect(
  () => {
    console.log(document.querySelector('#el')?.textContent)
  },
  { flush: 'post' }
)

小結


監聽在實戰上很常遇到,曾經踩過和看過的坑,以及效能問題也在這裡分享告一段落,總結就是...

  • watchEffect:快速響應、適合單純副作用。
  • watch:需要條件控制、比較精準的監聽。
  • 正確使用 flushonCleanup,能避免效能問題與資源洩漏。

參考資料


  1. 響應式 API:核心 watchEffect()
  2. 同步偵聽器 callback-flush-timing
  3. Lodash中文文档 - _.debounce(func, [wait=0], [options=])

上一篇
Day 8: 使用 computed 與 watch 資料監控與處理
系列文
Vue3.6 的革新:深入理解 Composition API9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言