Composition API 元件中的 setup() 函式可以作為非同步函式使用,在內部可以使用關鍵字 await 等待非同步陳述式執行。
一切聽起來很完美,但是!setup 遇到 await 後就會暫停、返回,返回後,元件的生命週期會繼續執行,這會衍生一些問題和注意事項。
註1:目前直接在 setup() 內使用 await,導致 setup 返回 Promise,Vue 會警告你要加上 <Suspense>,<Suspense> 會幫你處理接下來提到的問題。
註2:如何在元件非同步取得資料,並渲染到畫面上,請看上一篇。
在 await 陳述式之後,setup 函式會暫時返回,繼續跑元件的生命週期,所以在 async setup 中,應該將生命週期 Hooks 和部份 effects 的註冊,寫在 await 之前。
在 await 陳述式之後,部份 Vue API 的調用會失效:
以下函式會無法使用:
provide、inject
以下函式使用會有點缺陷:
watch、watchEffect
computed
最好理解的應該是「生命週期鉤子」的部份,因為 setup 返回之後,會繼續跑元件的生命週期,但後面的 Hooks 都還沒註冊完成,當然也沒有效果,對吧?
那為什麼這麼多方法會失效或有點缺點?核心原因是共通的,這些 API 需要綁定元件實例,在非同步期間註冊或初始化,會導致 Vue 找不到對應的元件實例。
這也是為什麼 Vue 在生命週期篇章特別說明,一定要讓生命週期被同步註冊,不可以這樣寫:
setTimeout(() => {
onMounted(() => {
// 异步注册时当前组件实例已丢失
// 这将不会正常工作
})
}, 100)
接下來從註冊生命週期 Hooks 的方式,去了解其中的運作機制,為什麼會找不到對應的元件實例。
以生命週期 Hooks - onMounted() 為例,開發者會將 callback function 傳入 onMounted,而這個 callback 會在元件掛載完成後被呼叫。
生命週期 Hooks 並不是掛在元件實例下的方法,他會直接被呼叫,也就是說呼叫的時候看不出來他的 context。
// 不是這樣直接掛在元件實例下,作為方法呼叫
component.onMounted(`callback function`)
// 是直接呼叫
onMounted(`callback function`)
所以像 onMounted() 要怎麼知道,現在是哪個元件剛掛載完成?
Vue 的作法是宣告一個全域變數,用來儲存剛掛載完成的元件實例,當 Hook 在 setup 函式中被調用時,他們就可以讀取外層宣告的全域變數,來拿到當前元件的實例。
let currentInstance = null
export function mountComponent(component) {
const instance = createComponent(component)
// hold the previous instance
const prev = currentInstance
// set the instance to global
currentInstance = instance
// hooks called inside the `setup()` will have
// the `currentInstance` as the context
component.setup()
// restore the previous instance
currentInstance = prev
}
export function onMounted(fn) {
if (!currentInstance) {
warn(`"onMounted" can't be called outside of component setup()`)
return
}
// bound listener to the current instance
currentInstance.onMounted(fn)
}
currentInstance = instance
component.setup()
currentInstance = prev
相信熟悉 Javascript 特性的人,已經知道為什麼會使用 await 會影響生命週期鉤子了。
Javascript 是單執行緒的程式語言,原則上會一行一行依序執行程式碼,但如果在 setup() 內使用 await,Javascript 會等待 await 後的非同步陳述式執行完畢,才會接著繼續處理。
currentInstance = instance
component.setup()
currentInstance = prev
async function setup() {
console.log(1)
const users = await getUsersData()
console.log(2)
onMounted(() => //執行內容-略//)
}
實際上的執行順序
currentInstance = instance
component.setup()
//console.log(1)
//然後就跑到 event loop 去等待拿回 usersData
currentInstance = prev
//有可能很久以後,也可能一下之後
//console.log(2)
//onMounted(() =>) 找不到當初的實例
Vue 也不知道非同步什麼時候會執行完畢,在非同步陳述式之後才調用 onMounted(Fn)的話,沒有辦法將元件實例綁定到 context 中。
watch、watchEffect、computed 非同步註冊的問題至於 watch、watchEffect、computed 的缺點是什麼。
就像在 watcher 篇章內提到的。
這三個監聽方法在 setup() 內同步註冊時,會綁定元件實例,這是為了在元件銷毀時,能停止元件內 watcher,避免 memory leak(不會用到的程式持續佔用憶體空間)。
如果沒有在 setup() 執行期間同步註冊,他們還是可以正常監聽,但因為沒有連動元件實例,無法在元件被銷毀時自動清除。
基本上,async setup() with await 還是要搭配 <Suspense> 或是其他函式庫提供的 composable 函式來處理。
不過,使用 async setup 的其中一個情境是,單純要將非同步取得的資料,就可以透過昨天提到的--「將非同步函式『變成』同步的響應式資料」來處理,這樣就可以避開 async setup。