vue中async-await的使用誤區
曾經見過為了讓鉤子函式的非同步程式碼可以同步執行,而對鉤子函式使用async/await,就好像下面的程式碼:
// exp-01 export default { async created() { const timeKey = 'cost'; console.time(timeKey); console.log('start created'); this.list = await this.getList(); console.log(this.list); console.log('end created'); console.timeEnd(timeKey); }, mounted() { const timeKey = 'cost'; console.time(timeKey); console.log('start mounted'); console.log(this.list.rows); console.log('end mounted'); console.timeEnd(timeKey); }, data() { return { list: [] }; }, methods: { getList() { return new Promise((resolve) => { setTimeout(() => { return resolve({ rows: [ { name: 'isaac', position: 'coder' } ] }); }, 3000); }); } } }; 複製程式碼
exp-01
的程式碼最後會輸出:
start created start mounted undefined end mounted mounted cost: 2.88623046875ms {__ob__: Observer} end created created cost: 3171.545166015625ms 複製程式碼
很明顯沒有達到預期的效果,為什麼?
根據 exp-01
的輸出結果,可以看出程式碼的執行順序,首先是鉤子的執行順序:
created => mounted 複製程式碼
是的,鉤子的執行順序還是正常的沒有被打亂,證據就是:created鉤子中的同步程式碼是在mounted先執行的:
start created start mounted 複製程式碼
再看看created鉤子內部的非同步程式碼:
this.list = await this.getList(); 複製程式碼
可以看見this.list的列印結果
end mounted mounted cost: 2.88623046875ms // 這是created鉤子列印的this.list {__ob__: Observer} end created 複製程式碼
在mounted鉤子執行完畢之後才打印,言外之意是使用async/await的鉤子內部的非同步程式碼並沒有起到阻塞鉤子主執行緒的執行。這裡說的鉤子函式的主執行緒是指:
beforeCreate => created => beforeMount => mounted => ... 複製程式碼
會寫出以上程式碼的原因我估計有兩個:
exp-01
正文
剖析一下
前言中針對程式碼的執行流程分析了一下,很明顯沒有如期望的順序執行,我們先來回顧一下期望的順序是什麼
// step 1 created() { // step 1.1 let endTime; const startTime = Date.now(); console.log(`start created: ${startTime}ms`); // step 1.2 this.list = await this.getList(); endTime = Date.now(); console.log(this.list); console.log(`end created: ${endTime}ms, cost: ${endTime - startTime}ms`); }, // step 2 mounted() { let endTime; const startTime = Date.now(); console.log(`start mounted: ${startTime}ms`); console.log(this.list.rows); endTime = Date.now(); console.log(`end mounted: ${endTime}ms, cost: ${endTime - startTime}ms`); } // step 1 => step 1.1 => step 1.2 => step 2 複製程式碼
期望的列印結果是:
// step 1(created) start created // this.list {__ob__: Observer} end created created cost: 3171.545166015625ms // step 2(mounted) start mounted // this.list.rows [{…}, __ob__: Observer] end mounted mounted cost: 2.88623046875ms 複製程式碼
對比實際的列印和期望的列印,就知道問題出在created鉤子內使用了await的非同步程式碼,並沒有達到我們期望的那種的“非同步程式碼同步執行”的效果,僅僅是一定程度上達到了這個效果。
下面來分析一下為什麼會出現這個非預期的結果!
在分析前,讓我們來回顧一下一些javascript的基礎知識!看看下面這段程式碼:
(function __main() { console.log('start'); setTimeout(() => { console.log('console in setTimeout'); }, 0); console.log('end'); })() // output start end console in setTimeout 複製程式碼
這個列印順序有沒有讓你想到什麼?!
任務佇列!

我們都知道JavaScript的程式碼可以分成兩類:
同步程式碼 和 非同步程式碼
同步程式碼會在主執行緒按照編寫順序執行;
非同步程式碼的觸發過程(注意是觸發,比如非同步請求的發起,就是在主執行緒同步觸發的)是同步的,但是非同步程式碼的實際處理邏輯(回撥函式)則會在非同步程式碼有響應時將處理邏輯程式碼推入任務佇列(也叫事件佇列),瀏覽器會在主執行緒(指當前執行環境的同步程式碼)程式碼執行完畢後以一定的週期檢測任務佇列,若有需要處理的任務,就會讓隊頭的任務出隊,推入主執行緒執行。
比如現在我們發起一個非同步請求:
// exp-02 console.log('start'); axios.get('http://xxx.com/getList') .then((resp) => { console.log('handle response'); }) .catch((error) => { console.error(error); }); console.log('end'); 複製程式碼
在主執行緒中,大概首先會發生如下過程:
// exp-03 // step 1 console.log('start'); // step 2 axios.get('http://xxx.com/getList');// 此時回撥函式(即then內部的邏輯)還沒有被呼叫 // step 3 console.log('end'); 複製程式碼
在看看瀏覽器此時在幹什麼!
此時事件輪詢(Event Loop)登場,其實並非此時才登場,而是一直都在!
“事件輪詢”這個機制會以一定的週期檢測任務佇列有沒有可執行的任務(所謂任務其實就是callback),有即出隊執行。
當 step 2
的請求有響應了,非同步請求的回撥函式就會被新增到任務佇列(Task Queue)或者 稱為 事件佇列(Event Queue),然後等到事件輪詢的下一次檢測任務佇列,佇列裡面任務就會依次出隊,進入主執行緒執行:即執行下面的程式碼:
// 假定沒有出錯的話 ((resp) => { console.log('handle response'); })() 複製程式碼
到此,簡短科普了任務佇列的機制,聯想 exp-01
的程式碼,大概知道出現非預期結果的原因了吧!
created鉤子中的await函式,雖然是在一定程度上是同步的,但是他還是被掛起了,實際的處理邏輯(this.list =resp.xxx)則在響應完成後才被新增進任務佇列,並且在主執行緒的同步程式碼執行完畢後執行。 下面是將延時時間設為0後的列印:
start created start mounted undefined end mounted mounted cost: 2.88623046875ms {__ob__: Observer} end created created cost: 9.76611328125ms 複製程式碼
這側面說明了await函式確實被被掛起,回撥被新增到任務佇列,在主執行緒程式碼執行完畢後等待執行。
然後是為什麼說 exp-01
的程式碼是一定程度的同步呢?!
同步執行的另一個意思是不是就是:阻塞當前執行緒的繼續執行直到當前邏輯執行完畢~
看看 exp-01
的列印:
{__ob__: Observer} end created created cost: 3171.545166015625ms 複製程式碼
end created
這句列印,是主執行緒的程式碼,如果是一般的非同步請求的話,這句列印應該是在 {__ob__: Observer}
這句列印之前的yo,至於為什麼會這樣,這裡就不多解析,自行google!
另外,這裡來個小插曲,你應該注意到,我一直強調,回撥函式被新增進任務佇列的時機是在響應完成之後,沒錯確實如此的!
但在不清除這個機制前,你大概會有兩種猜想:
- 在觸發非同步程式碼的時,處理邏輯就會被新增進任務佇列;
- 上面說到的,在非同步程式碼響應完成後,處理邏輯才會被新增進任務佇列;
其實大可推斷一下
佇列的資料結構特徵是:先進先出(First in First out)
此時假如主執行緒中有兩個非同步請求如下:
// exp-04 syncRequest01(callback01); syncRequest02(callback02); 複製程式碼
假設處理機制是第一點描述那樣,那麼callback01就會先被新增進任務佇列,然後是callback02。
然後,我們再假設syncRequest01的響應時間是10s,syncRequest02的響應時間是5s。
到這裡,有沒有察覺到違和感!
非同步請求的實際表現是什麼?是誰快誰的回撥先被執行,對吧!那麼實際表現就是callback02會先於callback01執行!
那麼基於這個事實,再看看上面的假設(callback01會執行)~
ok!插曲完畢!
解法
首先讓我回顧一下目的,路由元件對非同步請求返回的資料有強依賴,因此希望阻塞元件的渲染流程,待到非同步請求響應完畢之後再執行。
這就是我們需要做的事情,需要強調的一點是: 我們對資料有強依賴 ,言外之意就是資料沒有按預期返回,就會導致之後的邏輯出現不可避免的異常。
接下來,我們就需要探討一下解決方案!
元件內路由守衛瞭解一下!?
beforeRouteEnter beforeRouteUpdate (2.2 新增) beforeRouteLeave
這裡需要用到的路由守衛是: beforeRouterEnter
, 先看程式碼:
// exp-05 export default { beforeRouteEnter(to, from, next) { this.showLoading(); this.getList() .then((resp) => { this.hideLoading(); this.list = resp.data; next(); }) .catch((error) => { this.hideLoading(); // handle error }); }, mounted() { let endTime; const startTime = Date.now(); console.log(`start mounted: ${startTime}ms`); console.log(this.list.rows); endTime = Date.now(); console.log(`end mounted: ${endTime}ms, cost: ${endTime - startTime}ms`); }, }; 複製程式碼
路由守衛 beforeRouterEnter
,觸發這個鉤子後,主執行緒都會阻塞,頁面會一直保持假死狀態,直到在呼叫 beforeRouterEnter
的回撥函式 next
,才會跳轉路由進行新路由元件的渲染。
看起這個解決方案相當適合上面我們提出的需求,在呼叫 next
前,就可以去拉取資料!
但是如剛剛說到的,頁面在一直假死,加入資料獲取花費時間過長就難免變得很難看,使用者體驗未免太差
為此,在 exp-05
中我在請完成前後分別呼叫了 this.showLoading()
和 this.hideLoading()
以便頁面 keep-alive
。
這個處理假死的loading有沒有讓你想到寫什麼,沒錯就是下面這個github跳轉頁面是頂部的小藍條

想想就有點cool,當然還有很多的實現方式提升使用者體驗,比如作為body子元素的全屏loading,或者button-loading等等……
當然,我們知道阻塞主執行緒怎麼都是阻塞了,loading只是一種自欺欺人式的優化(此時這個成語可不是什麼貶義的詞語)!
因此,不是對資料有非常強的依賴,都應在路由的鉤子進行資料抓取,這樣就可以讓使用者“更快”地跳轉到目的頁。為避免頁面對資料依賴丟擲的異常(大概就是 undefined of xxx
),我們可以對初始資料進行一些預設,比如 exp-01
中對 this.list.rows
的依賴,我們可以預設 this.list
:
list: { rows: [] } 複製程式碼
這樣就不會丟擲異常,待到非同步請求完成,基於vue的update機制二次渲染我們的預期資料~
小結
對於 exp-01
的寫法,也不能說他是錯誤或不好的寫法,凡事都要看我們是出於什麼目的,如果僅僅是為了保證多個非同步函式的執行順序, exp-01
的寫法沒有任何錯誤,因此async/await不能用在路由鉤子上什麼的並不存在!
it just a tool!
ofollow,noindex">歡迎提出不同得見解,提bug和評論,我們issue相見