JS promise 的回調(diào)和 setTimeout 的回調(diào)到底誰先執(zhí)行
首先提一個小問題:運行下面這段 JS 代碼后控制臺的輸出是什么?
console.log("script start"); setTimeout(function () { console.log("setTimeout1"); }, 0); new Promise((resolve, reject) => { setTimeout(function () { console.log("setTimeout2"); resolve(); }, 100); }).then(function () { console.log("promise1"); }); Promise.resolve() .then(function () { console.log("promise2"); }) .then(function () { console.log("promise3"); }); console.log("script end");
可以先嘗試自己分析一下結(jié)果,然后再看答案:
script start
script end
promise2
promise3
setTimeout1
setTimeout2
promise1
怎么樣,你猜對了嗎?如果對這個輸出結(jié)果感到很迷惑,這篇文章或許可以幫到你。
PS:文中按照標準分析理論結(jié)果,但實際上各個瀏覽器對任務隊列的支持情況很混亂,所以如果你在瀏覽器執(zhí)行代碼后發(fā)現(xiàn)結(jié)果不同也不必糾結(jié);總體來說 Chrome 的支持比較好。
如果對 Promise 的用法還不熟悉,可以看我的上一篇博客:前端 | JS Promise:axios 請求結(jié)果后面的 .then() 是什么意思?
任務 VS 微任務
JavaScript 設計的本質(zhì)是單線程語言,但隨著硬件性能的飛速發(fā)展,純單線程已經(jīng)不太能夠滿足需求了。因此 JS 逐漸發(fā)展出了任務和微任務,來模擬實現(xiàn)多線程。
瀏覽器中,對于每個網(wǎng)頁(有時也可能是多個同源網(wǎng)頁),網(wǎng)頁的代碼和瀏覽器自身的用戶界面程序運共享同一個主線程,它除了運行瀏覽器交給它的 JS 代碼,也負責收集和派發(fā)事件、渲染和繪制網(wǎng)頁內(nèi)容等等。因此,如果主線程中的某個任務阻塞了,其他任務都會受到影響;這就是為什么有時候網(wǎng)頁代碼出現(xiàn)了錯誤會導致整個網(wǎng)頁渲染失敗。
每個主線程都由一個事件循環(huán) Event loops 驅(qū)動。事件循環(huán)可以理解為一個任務隊列,JS 引擎不斷的進行“循環(huán)-等待”,按順序處理隊列中的任務。事件循環(huán)中的任務稱作“任務 Task”,由宿主環(huán)境(瀏覽器)創(chuàng)建;每個任務都是宿主計劃執(zhí)行的 JavaScript 代碼,如程序初始化、解析HTML、事件觸發(fā)的回調(diào)(例如點擊網(wǎng)頁上的按鈕),或是由 setTimeout()
setInterval()
等 API 添加的回調(diào)函數(shù)。
JS 引擎在執(zhí)行一個任務的過程中,有時會進行一些異步操作,不會立即執(zhí)行,但又想在同一個任務中完成、不留到事件循環(huán)中的下一個任務里;例如常用的 promise、監(jiān)控 DOM 的回調(diào)等。這時,JS 引擎會創(chuàng)建一個“微任務 Mircotask”,并加入當前的微任務隊列中。(有時為了區(qū)分,也把任務task稱為“宏任務”。)
事件循環(huán)、任務、微任務的示意圖如下:
執(zhí)行過程
一個主線程的執(zhí)行過程如下:
- 拿出事件循環(huán)中的下一個任務
- 執(zhí)行任務本身的 Script 代碼;期間可能會往任務隊列、微任務隊列創(chuàng)建添加新任務
- script 執(zhí)行完后,檢查微任務隊列
- 如果有微任務,順序執(zhí)行,期間可能還會創(chuàng)建新的任務和微任務
- 如果微任務隊列為空,這個任務執(zhí)行結(jié)束,回到第一步
可以看出,在一個任務中會反復檢查微任務隊列,直到?jīng)]有微任務存在了才會執(zhí)行下一個任務。因此在任務腳本和微任務腳本中創(chuàng)建的所有微任務都會在這個任務結(jié)束前執(zhí)行,同時也意味著會早于其他所有創(chuàng)建的任務執(zhí)行(因為新建的任務都加入了任務隊列)。
案例分析
明白了任務和微任務的區(qū)別,下面再來看文章開頭的例子:
console.log("script start"); setTimeout(function () { console.log("setTimeout1"); }, 0); new Promise((resolve, reject) => { setTimeout(function () { console.log("setTimeout2"); resolve(); }, 100); }).then(function () { console.log("promise1"); }); Promise.resolve() .then(function () { console.log("promise2"); }) .then(function () { console.log("promise3"); }); console.log("script end");
接下來逐步跟蹤代碼的執(zhí)行過程;如果感覺文字不夠直觀,可以看這篇博客中給出的逐步執(zhí)行動畫。
整個 Script 會被宿主環(huán)境傳給 JS 引擎,作為任務隊列中的一個任務;首先執(zhí)行任務中的腳本代碼:
- line1:
console.log("script start")
是同步代碼,直接輸出 - line3: 執(zhí)行
setTimeout()
,在0秒后將console.log("setTimeout1");
加入任務隊列 - line8: 執(zhí)行
setTimeout()
,在0.1秒后將console.log("setTimeout2");
和resolve()
加入任務隊列 - line16: 返回一個已成功的 promise,第一個 then 回調(diào)被加入微任務隊列
- line24:
console.log("script end")
是同步代碼,直接輸出 - 任務 script 執(zhí)行完畢
此時:
- 控制臺輸出了
script start
script end
- 任務隊列中(除當前任務以外)有2個任務(兩個
setTimeout()
的回調(diào)按時間先后順序排列) - 微任務隊列中有1個任務(promise 的回調(diào))
接下來檢查微任務隊列,執(zhí)行隊首的微任務:
console.log("promise2")
輸出- 隱式 return,相當于返回一個
Promise.resolve(undefined)
;因此 Promise 鏈中的下一個 then 回調(diào)被加入微任務隊列 - 微任務執(zhí)行完畢
此時:
- 控制臺輸出了
script start
script end
promise2
- 任務隊列中(除當前任務以外)有2個任務(兩個
setTimeout()
的回調(diào)按時間先后順序排列) - 微任務隊列中有1個任務(第二個 promise 回調(diào))
再次檢查微任務隊列,執(zhí)行隊首的微任務:
console.log("promise3")
輸出- 隱式 return(但此時 Promise 鏈已經(jīng)結(jié)束了,所以無事發(fā)生)
- 微任務執(zhí)行完畢
此時:
- 控制臺輸出了
script start
script end
promise2
promise3
- 任務隊列中(除當前任務以外)有2個任務(兩個
setTimeout()
的回調(diào)按時間先后順序排列) - 微任務隊列為空
檢查微任務隊列,發(fā)現(xiàn)沒有微任務了,當前任務結(jié)束;開始執(zhí)行任務隊列中的下一個任務(0秒后執(zhí)行的回調(diào)):
console.log("setTimeout1");
輸出- 任務 script 執(zhí)行完畢
此時:
- 控制臺輸出了
script start
script end
promise2
promise3
setTimeout1
- 任務隊列中(除當前任務以外)有1個任務
- 微任務隊列為空
檢查微任務隊列,發(fā)現(xiàn)沒有微任務,當前任務結(jié)束;開始執(zhí)行任務隊列中的下一個任務(0.1秒后執(zhí)行的回調(diào)):
console.log("setTimeout2");
輸出resolve();
將 promise 的狀態(tài)更改為已成功;then 回調(diào)被加入微任務隊列- 任務 script 執(zhí)行完畢
此時:
- 控制臺輸出了
script start
script end
promise2
promise3
setTimeout1
setTimeout2
- 任務隊列中只有當前任務
- 微任務隊列中有一個任務(promise 的回調(diào))
檢查微任務隊列,執(zhí)行隊首的微任務:
console.log("promise1")
輸出- 隱式 return(但此時 Promise 鏈已經(jīng)結(jié)束了,所以無事發(fā)生)
- 微任務執(zhí)行完畢
此時:
- 控制臺輸出了
script start
script end
promise2
promise3
setTimeout1
setTimeout2
promise1
- 任務隊列中只有當前任務
- 微任務隊列為空
檢查微任務隊列,發(fā)現(xiàn)沒有微任務,當前任務結(jié)束。任務隊列中沒有其他任務,執(zhí)行完畢。
結(jié)語 & 參考資料
異步操作已經(jīng)是平時開發(fā)過程中不可避免經(jīng)常會遇到的用法了,平時都是馬馬虎虎的用,最近終于認真學習了一下,感覺頗有收獲。不過話說回來,理論學習和實際開發(fā)畢竟存在差異。首先各種瀏覽器的支持只能說是慘不忍睹,所以真實開發(fā)過程中不能太過依賴理論分析的結(jié)果,需要實際測試代碼功能的兼容性;另一方面,過于復雜的嵌套異步操作,容易造成沒必要的錯誤,同時導致代碼很難理解和維護,能不用最好不用,KISS。
以上是個人學習JS的任務/微任務機制時的一些思考和總結(jié),希望能對你有所幫助;文中可能存在疏漏和錯誤,敬請討論和指正。
Tasks, microtasks, queues and schedules
到此這篇關(guān)于JS promise 的回調(diào)和 setTimeout 的回調(diào)到底誰先執(zhí)行 的文章就介紹到這了,更多相關(guān)JS promise 的回調(diào)和 setTimeout 的回調(diào)執(zhí)行 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于JS代碼實現(xiàn)簡單易用的倒計時 x 天 x 時 x 分 x 秒效果
這篇文章主要介紹了基于JS代碼實現(xiàn)簡單易用的倒計時 x 天 x 時 x 分 x 秒效果,需要的朋友可以參考下2017-07-07js判斷復選框是否選中及選中個數(shù)的實現(xiàn)代碼
下面小編就為大家?guī)硪黄猨s判斷復選框是否選中及選中個數(shù)的實現(xiàn)代碼。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-05-05canvas+gif.js打造自己的數(shù)字雨頭像的示例代碼
本篇文章主要介紹了canvas+gif.js打造自己的數(shù)字雨頭像的示例代碼,這里整理了詳細的代碼,非常具有實用價值,需要的朋友可以參考下2017-10-10javascript實現(xiàn)div的顯示和隱藏的小例子
這篇文章介紹了在JS中實現(xiàn)DIV顯示和隱藏的實例,需要的朋友可以參考一下2013-06-06解析JavaScript中的不可見數(shù)據(jù)類型
這篇文章主要是對JavaScript中的不可見數(shù)據(jù)類型進行了詳細的介紹,需要的朋友可以過來參考下,希望對大家有所幫助2013-12-12