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