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
來控制執行時機,以及 onTrack
、onTrigger
用於響應式追蹤的回撥。
<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>
watchEffect
與 watch
的區別一樣都是追蹤,那 watch
和 watchEffect
有什麼不同呢?
讓我們依照不同特性來觀察 watch
和 watchEffect
的差別:
特性 | watch |
watchEffect |
---|---|---|
依賴收集 | 手動指定 | 自動收集 |
適合場景 | 複雜邏輯,需要 new/old 值 | 快速聲明副作用 |
清理方式 | 回呼內自行處理 | 內建 onCleanup |
執行時機 | 初始不執行(可設定) | 初始就執行一次 |
watch
需要 明確指定 依賴,適合需求較為複雜的場景。watchEffect
自動追蹤 依賴,適合邏輯相對簡單且依賴強相關的情況。
watchEffect
副作用 (Side Effect)今天一開頭就提到 watchEffect
是用來處理副作用,那...為什麼會有副作用?副作用會影響什麼呢?
讓我們繼續看下去...
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 個選項:
pre
→ 搶在渲染前先跑。sync
→ 反應最快,但 DOM 還是舊的。post
→ 等畫面更新完再跑,最適合需要讀取 DOM 的情境。![]()
資源洩漏 (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
:需要條件控制、比較精準的監聽。flush
與 onCleanup
,能避免效能問題與資源洩漏。_.debounce(func, [wait=0], [options=])