我們學了如何打造「單一職責、可組合」的 composable,並且讓多個 composable 一起運作時,充分發揮 Vue 3.6 的效能優勢。
但如果我們拆出了許多小 composable、又同時組裝在一起,就會遇到一個問題:誰來負責清理這些副作用?
今天我們深入 Composition API 的核心:作用域管理 (Scope Management),看看 Vue 3.6 如何在 runtime 層面幫我們做得更快、更乾淨。
在 Vue 中,每個元件都是一個響應式作用域 (Reactive Scope):
然而,當我們開始拆分 composable,狀況變得更複雜:
Vue 3.6 在 runtime 中,進一步優化了作用域的建立與銷毀流程,讓系統在清理多層巢狀 composable 時更輕量,避免多餘的依賴重算。
onScopeDispose
: composable 的專屬清理鈎子在 composable 裡,我們通常使用 onScopeDispose
來清理副作用。
但是多數上還是使用 onUnmounted
, onScopeDispose
和 onUnmounted
有什麼差異呢?
Hook | 清理時機 | 典型用途 |
---|---|---|
onUnmounted |
元件卸載時清理 | 元件內 watch 、setInterval |
onScopeDispose |
呼叫 composable 的 scope 銷毀時清理 | composable 內部的副作用 |
import { ref, onScopeDispose } from 'vue'
export function useMouse() {
const x = ref(0), y = ref(0)
function update(e: MouseEvent) {
x.value = e.pageX
y.value = e.pageY
}
window.addEventListener('mousemove', update)
// 重點:跟呼叫者的 effect scope 綁定
onScopeDispose(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}
如果這個 composable 在一個
<script setup>
內被呼叫,當元件卸載時會自動清理。
如果它被另一個 composable 呼叫,只要呼叫的人 scope 結束,也能正確清理。
effectScope
有時候,我們需要建立脫離元件生命週期的「臨時作用域」,動態啟動/停止副作用 (例如:切換 tab、開啟 modal 時才啟動計時器),或是在非元件環境 (例如 Pinia store) 中手動管理資源。
這時可以使用 effectScope
直接創建一個獨立 scope:
import { effectScope, ref, watch } from 'vue'
// 建立一個獨立的 scope
const scope = effectScope()
scope.run(() => {
const count = ref(0)
watch(count, () => console.log('changed:', count.value))
setInterval(() => count.value++, 1000)
})
// 任何時候都可以手動停止
scope.stop() // 停止後,自動清理所有 watch / effect
Vue 3.6 在 runtime 優化了巢狀 scope的建立與銷毀:
export function useAutoRefresh(api: () => Promise<any>) {
const data = ref<any>(null)
const timer = setInterval(async () => {
data.value = await api()
}, 3000)
// 無論是元件還是其他 composable 清理,每個 onScopeDispose 只會清理自己的範圍
onScopeDispose(() => clearInterval(timer))
return { data }
}
不管是前台還是後台,設計很常會有頁籤切換內容,以下示範一個「進入 Tab → 啟動輪詢、離開 → 自動清理」的場景:
import { effectScope, ref } from 'vue'
import { useAutoRefresh } from './useAutoRefresh'
export function useTabData() {
// 作用域內所有建立的 reactive / watch / computed / 自動副作用,都會被 effectScope 記錄
const current = ref('home')
let tabScope: ReturnType<typeof effectScope> | null = null
function enterTab(name: string) {
current.value = name
tabScope?.stop() // 清理前一個 tab 整個 scope 裡的副作用會一次清掉(例如定時器、watcher)
tabScope = effectScope()
tabScope.run(() => {
useAutoRefresh(() => fetch(`/api/${name}`))
})
}
function leaveTab() {
tabScope?.stop()
tabScope = null
}
return { current, enterTab, leaveTab }
}
Vue 3.6 在 runtime 中對 effect scope 與依賴追蹤 做了以下優化: