在理解事件迴圈之前,我總會遇到一些奇奇怪怪的問題:比如明明已經調介面拿到了資料,可是跟在調資料之後的操作卻沒有正常執行;又或者不知道為啥,程式碼裡非得加個setTimeout
才能正常跑通;特別是在運用Promise的時候,更是有各種問題百思不得解。遇上問題要解決,更要知道問題產生的原因,這樣才能hold住全場!
廢話不多說了,先來看一段程式碼
console.log(`start`);
setTimeout(function(){
console.log(`setTImeout1`)
},0);
new Promise(function(resolve,reject){
console.log(`resolve`)
setTimeout(function(){
console.log(`setTimeout2`)
},200);
resolve()
}).then(function(){
console.log(`then`)
});
setTimeout(function(){
console.log(`setTimeout3`)
},0);
console.log(`end`);
複製程式碼
結果是start resolve end then setTimeout1 setTimeout3 settimeout2
。
在分析結果之前,我先來科普幾個概念,這些概念的表述不一定與標準完全對應,但是可以幫助你更容易理解JS的事件機制
- 巨集任務(macro-task):包括js整體程式碼,setTimeout,setInterval,setImmediate ,I/O, UI renderder等
- 微任務(micro-task):包括Promise,Object.observe,process.nextTick,MutationObserver等
- 呼叫棧:js被載入進來之後,會從上至下讀取程式碼,同步程式碼被立即執行,而非同步程式碼被加入事件佇列中
- 事件佇列:一些沒有被立即執行的程式碼被新增到事件佇列中,佇列是一種先進先出的資料結構,也就是說,先加入事件佇列的任務會被優先執行
我們知道,js是單執行緒的,這就是說,只有一個主執行緒,主執行緒會自上而下依次執行呼叫棧中的事件。任務佇列中的程式碼被載入到函式呼叫棧中去執行。當前的巨集任務佇列中的程式碼執行完畢後,會執行本次巨集任務佇列中分發到微任務佇列中的程式碼。然後執行下一個巨集任務佇列中的程式碼,依次迴圈。
這裡要提一點容易誤解的地方,setTimeout
函式本身,其實是立即執行的,它內部的任務,才會被分發到任務佇列中延時執行。
- 程式碼被載入後,全域性上下文進入函式呼叫棧,緊接著,‘start’被執行
- 遇到
setTimeout
的時候,新建了一個巨集任務佇列,函式內的任務被分發這個佇列中等待執行 - 此時遇到了
Promise
,注意,Promise
中的第一個function中的程式碼會立馬開始執行,遇到resolve
或者reject
後,then
方法中函式會被分發到本次事件迴圈的微任務佇列中等待執行。所以這裡立馬列印出了`resolve`。遇到setTimeout2
後,同樣新建了一個巨集任務佇列,其中的函式被分發到了這個新的巨集任務佇列中,then
方法中的操作被分發到了微任務佇列中等待 - 程式碼繼續往下,遇到`setTimeout3`後再次新建了一個新的巨集任務佇列
- `end`被立即執行。此時有三個巨集任務佇列,一個微任務佇列
- 微任務佇列中的操作被執行,也就是列印出了‘then’,此時,第一輪的事件迴圈結束。
- 第一輪的事件迴圈結束,開始下一輪的事件迴圈,依次執行每個巨集任務佇列中的內容,我們這裡巨集任務佇列中的函式比較簡單,都是
console
操作,所以並沒有再分發新的任務佇列,但是由於第二個setTimeout
設定了200毫秒的延時,所以‘setTimeout2’被最後列印。
說到這裡,你基本上對事件迴圈有個大致的瞭解了。之前有個同學問過我一個問題,點選輪播圖下一頁,但是頁面沒有反應,程式碼是這樣的:
這個函式是點選下一頁的按鈕後輪播圖轉動,他在getNextPhoto
函式中調介面獲取了下個頁面的資料,goToPage
函式裡是讓輪播圖切換的操作。相信如果你讀懂了這篇文章,就會知道問題出在了哪裡。顯然獲取資料ajax
是個非同步操作,他被分發到了事件佇列中等待執行,所以還沒等資料回來,翻頁的操作已經開始執行,由於沒有資料,並沒有按預期效果顯示。解決方法就是在getNextPhoto
函式裡調介面拿到資料之後,再通知去執行goToPage
操作,問題就解決了。
作為一個前端菜鳥,希望得到各位大神的批評指正!