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
。