JavaScript中的異步代碼
JavaScript是一個(gè)單線程非阻塞的腳本語(yǔ)言。這代表代碼是執(zhí)行在一個(gè)主線程上面的。但是JavaScript中有很多耗時(shí)的異步操作,例如AJAX,setTimeout等等;也有很多事件,例如用戶觸發(fā)的點(diǎn)擊事件,鼠標(biāo)事件等等。這些異步操作并不會(huì)阻塞我們代碼的執(zhí)行。例如:
let a = 1;
setTimeout(() => {
console.log('->', a)
}, 10);
a = 2;
// 輸出 -> 2
可以看到,上述代碼在瀏覽器中執(zhí)行時(shí),遇到setTimeout操作,并沒(méi)有阻塞等待異步操作的結(jié)束再繼續(xù)執(zhí)行代碼,而是先繼續(xù)執(zhí)行后面的代碼。等異步操作結(jié)束后,瀏覽器再回來(lái)執(zhí)行異步回調(diào)中的代碼。因此,上述代碼的console.log輸出時(shí),a的值已經(jīng)變?yōu)榱?。
這些異步非阻塞的實(shí)現(xiàn),就是靠Javascript中的事件循環(huán)機(jī)制。
JavaScript中的線程
上面說(shuō)到JavaScript是一個(gè)單線程的語(yǔ)言,這句話并不完全對(duì)。單線程指的是代碼在一個(gè)主線程中運(yùn)行,但是代碼所觸發(fā)的任務(wù)不一定在主線程運(yùn)行。除了執(zhí)行代碼的線程之外,執(zhí)行JavaScript的環(huán)境中還包含其他很多線程。其中瀏覽器的線程與Node.js中的線程也不相同。
瀏覽器中的線程
注意,這里對(duì)于瀏覽器線程進(jìn)行了抽象和總結(jié)。實(shí)際上瀏覽器的線程和進(jìn)程要更復(fù)雜,而且有時(shí)候會(huì)根據(jù)瀏覽器版本的不同而變化,因此僅供參考。
- JS主線程
負(fù)責(zé)運(yùn)行JavaScript代碼,解析HTML,CSS,構(gòu)建DOM樹(shù),布局和繪制頁(yè)面等等。 - 事件監(jiān)聽(tīng)線程
負(fù)責(zé)監(jiān)聽(tīng)觸發(fā)的各種事件,放入事件循環(huán)中。 - HTTP請(qǐng)求線程
負(fù)責(zé)處理各類網(wǎng)絡(luò)請(qǐng)求。 - 定時(shí)觸發(fā)器線程
為setInterval,setTimeout定時(shí)觸發(fā)操作等操作進(jìn)行定時(shí)計(jì)數(shù)的線程。
瀏覽器中的進(jìn)程
上面的線程實(shí)際上都在瀏覽器中的渲染進(jìn)程中包含。一個(gè)瀏覽器要想正常運(yùn)行,只做上述的操作是不夠的。我們以Chrome為例,列舉一個(gè)瀏覽器運(yùn)行所需要的進(jìn)程。
- 瀏覽器進(jìn)程
負(fù)責(zé)網(wǎng)頁(yè)外的界面功能,例如地址欄,書(shū)簽等等。 - GPU進(jìn)程
負(fù)責(zé)使用GPU渲染界面。 - 網(wǎng)絡(luò)進(jìn)程
負(fù)責(zé)網(wǎng)絡(luò)相關(guān)的請(qǐng)求處理。 - 插件進(jìn)程
負(fù)責(zé)瀏覽器插件運(yùn)行。 - 渲染進(jìn)程
負(fù)責(zé)網(wǎng)頁(yè)內(nèi)頁(yè)面展示相關(guān)的操作,即上一節(jié)瀏覽器中的線程包含的所有線程都在這個(gè)進(jìn)程中執(zhí)行。
一個(gè)瀏覽器可以擁有多個(gè)標(biāo)簽頁(yè),在不同的標(biāo)簽頁(yè)中,除了渲染進(jìn)行之外,都是共享的。即我們打開(kāi)一個(gè)新的標(biāo)簽頁(yè)時(shí),會(huì)產(chǎn)生一個(gè)新的渲染進(jìn)程。(當(dāng)在原標(biāo)簽頁(yè)中打開(kāi)新標(biāo)簽頁(yè),且屬于同一個(gè)域則共享一個(gè)渲染進(jìn)程)
進(jìn)程與線程的關(guān)系
上面我們了解了瀏覽器中的進(jìn)程和線程,有些同學(xué)就會(huì)有疑問(wèn),為什么要設(shè)立這么多的進(jìn)程和線程?
進(jìn)程是操作系統(tǒng)分配資源的基本單位,而線程是CPU任務(wù)調(diào)度和執(zhí)行的基本單位。
簡(jiǎn)單理解下就是一個(gè)完整的應(yīng)用程序是以進(jìn)程為單位的,即至少有一個(gè)進(jìn)程。而一段程序/代碼在CPU的獨(dú)立執(zhí)行則至少以線程為單位。不同的進(jìn)程和不同的線程都可以并行運(yùn)行。
一個(gè)進(jìn)程可以包含很多個(gè)線程,多個(gè)線程共享一個(gè)進(jìn)程的資源(比如內(nèi)存)。當(dāng)一個(gè)進(jìn)程崩潰后不會(huì)影響其他進(jìn)程,但是當(dāng)一個(gè)線程崩潰,它所在的整個(gè)進(jìn)程都會(huì)崩潰掉,這個(gè)進(jìn)程內(nèi)的其他線程也會(huì)崩潰。
因此,為了同時(shí)并行執(zhí)行代碼和異步請(qǐng)求,瀏覽器中的渲染進(jìn)程包含很多線程來(lái)并行運(yùn)行任務(wù)。而為了讓不同標(biāo)簽頁(yè)的網(wǎng)頁(yè)不互相影響,不同標(biāo)簽頁(yè)擁有獨(dú)立的渲染進(jìn)程。這樣即使某個(gè)網(wǎng)頁(yè)崩潰,也不會(huì)影響其他標(biāo)簽頁(yè)。
Node.js中的線程
- JS主線程
負(fù)責(zé)運(yùn)行JavaScript代碼。 - libuv的異步I/O線程池
負(fù)責(zé)實(shí)現(xiàn)事件循環(huán)和異步IO等操作,在不同操作系統(tǒng)的具體實(shí)現(xiàn)方式不同。 - 用戶創(chuàng)建的線程
上述這些進(jìn)程和線程的說(shuō)明也僅僅是進(jìn)行了抽象和簡(jiǎn)化,事實(shí)上瀏覽器和Node.js中的進(jìn)程和線程數(shù)要更多,處理也更復(fù)雜。
宏任務(wù)與微任務(wù)
Javascript中的異步任務(wù)大致可以分為兩種:宏任務(wù)和微任務(wù)。宏任務(wù)和微任務(wù)的執(zhí)行順序和優(yōu)先級(jí)是不同的,具體的執(zhí)行順序問(wèn)題我們?cè)谑录h(huán)中描述,這里先來(lái)看一下,哪些操作屬于宏任務(wù),哪些屬于微任務(wù)。這里僅僅是簡(jiǎn)單介紹,更詳細(xì)的要在了解事件循環(huán)之后說(shuō)明。
宏任務(wù)
任務(wù) | 瀏覽器 | Node.js | 描述 |
---|---|---|---|
setTimeout | ? | ? | 在指定的毫秒數(shù)后調(diào)用函數(shù) |
setInterval | ? | ? | 定時(shí)調(diào)用函數(shù) |
script標(biāo)簽 | ? | 整體代碼塊 | |
I/O請(qǐng)求 | ? | ? | 例如文件請(qǐng)求,網(wǎng)絡(luò)請(qǐng)求等 |
DOM事件 | ? | 例如點(diǎn)擊事件,hover事件等 | |
requestAnimationFrame | ? | 瀏覽器重繪前更新動(dòng)畫(huà) | |
postMessage | ? | iframe跨域通信 | |
MessageChannel | ? | ? | 管道通信 |
setImmediate | ? | 一次事件循環(huán)執(zhí)行完畢調(diào)用 |
微任務(wù)
任務(wù) | 瀏覽器 | Node.js | 描述 |
---|---|---|---|
Promise中resolve和reject回調(diào) | ? | ? | |
async函數(shù)中的await異步函數(shù) | ? | ? | |
MutationObserver | ? | 監(jiān)聽(tīng)DOM變動(dòng)觸發(fā) | |
process.nextTick | ? | 當(dāng)前任務(wù)結(jié)束后執(zhí)行 |
事件循環(huán)
與上面進(jìn)程與線程的介紹一樣,在瀏覽器中與Node.js中實(shí)現(xiàn)循環(huán)的方式也并不相同。下面我們來(lái)分別簡(jiǎn)單介紹一下。注意,這僅僅是對(duì)執(zhí)行邏輯的抽象和總結(jié),實(shí)際上瀏覽器和Node.js中的實(shí)現(xiàn)要更復(fù)雜。
瀏覽器中的事件循環(huán)
瀏覽器中的事件循環(huán)可以分為兩個(gè)隊(duì)列,宏任務(wù)隊(duì)列和微任務(wù)隊(duì)列。具體的任務(wù)執(zhí)行順序如下:
- 解析HTML中遇到script標(biāo)簽,開(kāi)始執(zhí)行第一個(gè)宏任務(wù)。
- 在宏任務(wù)執(zhí)行中遇到宏任務(wù),執(zhí)行其中的請(qǐng)求(例如網(wǎng)絡(luò)請(qǐng)求,定時(shí)器),在請(qǐng)求完成后將回調(diào)放入宏任務(wù)隊(duì)列中。
- 在宏任務(wù)執(zhí)行中遇到微任務(wù),暫不執(zhí)行回調(diào),而是放入微任務(wù)隊(duì)列中。
- 宏任務(wù)執(zhí)行完成。開(kāi)始依次執(zhí)行微任務(wù)隊(duì)列中的任務(wù)。
- 微任務(wù)執(zhí)行中遇到宏任務(wù)或者微任務(wù),處理方式同上,分別放入各自的隊(duì)列中。
- 微任務(wù)隊(duì)列清空后,開(kāi)始執(zhí)行宏任務(wù)隊(duì)列中的下一個(gè)任務(wù)。
在事件循環(huán)的流程中,微任務(wù)的優(yōu)先級(jí)實(shí)際上更高,執(zhí)行完一個(gè)宏任務(wù)之后,要執(zhí)行微任務(wù)隊(duì)列中的所有任務(wù)。
為什么要區(qū)分宏任務(wù)和宏任務(wù),優(yōu)先級(jí)也不同
因?yàn)椴煌蝿?wù)的開(kāi)銷不同,有的任務(wù)需要調(diào)用不同的線程甚至進(jìn)程,有的任務(wù)需要等待請(qǐng)求返回甚至定時(shí)。
- 如果將全部的任務(wù)同步執(zhí)行,那些耗時(shí)較久的任務(wù)會(huì)阻塞,造成整個(gè)頁(yè)面加載緩慢。假設(shè)有請(qǐng)求A耗時(shí)10秒,請(qǐng)求B耗時(shí)20秒,如果同步執(zhí)行,需要耗費(fèi)30秒。如果將請(qǐng)求由其它線程實(shí)現(xiàn),回調(diào)放入宏任務(wù),則執(zhí)行流程變?yōu)椋簣?zhí)行代碼->碰到A請(qǐng)求,其他線程異步等待返回->繼續(xù)執(zhí)行代碼->碰到b請(qǐng)求,其他線程異步等待返回。A和B就實(shí)現(xiàn)了異步請(qǐng)求,回調(diào)被分別放入宏任務(wù),等待下次事件循環(huán)。耗時(shí)間為20秒。
- 為什么微任務(wù)的優(yōu)先級(jí)更高?因?yàn)槲⑷蝿?wù)大部分是耗時(shí)不太久,不需要等待其他線程/進(jìn)程等待完成通知的。因此,微任務(wù)相當(dāng)于在宏任務(wù)的基礎(chǔ)上進(jìn)行了“插隊(duì)”,擁有更高的優(yōu)先級(jí),也提高了頁(yè)面的響應(yīng)速度。
為什么script標(biāo)簽是宏任務(wù)呢?
- script標(biāo)簽可能需要異步請(qǐng)求獲取,例如
<script src="myscripts.js"></script>
。 - script標(biāo)簽是嵌入在HTML中的,瀏覽器需要將HTML中的script標(biāo)簽解析出來(lái)供執(zhí)行,這個(gè)步驟需要耗費(fèi)一定的時(shí)間。
瀏覽器事件循環(huán)的更多說(shuō)明
WHATWG(網(wǎng)頁(yè)超文本應(yīng)用技術(shù)工作小組)在官網(wǎng)對(duì)事件循環(huán)和任務(wù)隊(duì)列做出了更詳細(xì)的說(shuō)明和解釋,可以作為參考:說(shuō)明文檔。在新的說(shuō)明中,任務(wù)的分類和事件循環(huán)已經(jīng)有了部分區(qū)別,這里簡(jiǎn)要說(shuō)一下,更多還請(qǐng)直接查看文檔:
- 事件循環(huán)不一定對(duì)應(yīng)于多線程。例如多個(gè)事件循環(huán)可以在單個(gè)線程中協(xié)作調(diào)度。
- 任務(wù)隊(duì)列并不是一個(gè)嚴(yán)格的隊(duì)列,而是一個(gè)集合。每次從隊(duì)列中取出一個(gè)可以被執(zhí)行的任務(wù),而不是選取第一個(gè)任務(wù)(可能該任務(wù)還在阻塞中)。
- 宏任務(wù)隊(duì)列有多個(gè),不同類型的任務(wù)(任務(wù)源)放置在不同的任務(wù)隊(duì)列中。具體的選取規(guī)則瀏覽器根據(jù)實(shí)際情況確定。
Node.js中的宏任務(wù)隊(duì)列
Node.js的官網(wǎng)給出了事件循環(huán)的文檔。它的事件循環(huán)要比瀏覽器的看起來(lái)復(fù)雜一些。下面是Node.js的宏任務(wù)隊(duì)列。
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
Node.js的宏任務(wù)隊(duì)列并不是一整個(gè)隊(duì)列,而是根據(jù)事件類型做出了區(qū)分,分為了六個(gè)隊(duì)列,依次執(zhí)行:
-
timers
定時(shí)器隊(duì)列,執(zhí)行定時(shí)器的回調(diào) -
pending callbacks
掛起的回調(diào)函數(shù),用于某些系統(tǒng)回調(diào) -
idle, prepare
僅在內(nèi)部使用 -
poll
執(zhí)行I/O事件回調(diào) -
check
setImmediate回調(diào) -
close callbacks
close事件的回調(diào),例如socket.on('close', ...)
其中我們的大部分宏任務(wù)回調(diào)都會(huì)在poll
階段執(zhí)行,除了timers
、check
和close callbacks
階段的特殊回調(diào)。每個(gè)宏任務(wù)隊(duì)列都有自己的微任務(wù)隊(duì)列。
Node.js事件循環(huán)的流程
- 首先執(zhí)行主線代碼,遇到宏任務(wù)就分配到對(duì)應(yīng)的宏任務(wù)隊(duì)列中,微任務(wù)也劃分到主線的微任務(wù)隊(duì)列中,直到執(zhí)行完畢。
- 執(zhí)行主線代碼的微任務(wù)隊(duì)列中的所有任務(wù)。
- 沒(méi)有宏任務(wù)則執(zhí)行結(jié)束,有則開(kāi)始事件循環(huán)。在事件循環(huán)中,按照上述的6個(gè)宏任務(wù)隊(duì)列依次執(zhí)行。下面的步驟是單個(gè)隊(duì)列中的流程。
- 在單個(gè)宏任務(wù)隊(duì)列中,選擇一個(gè)宏任務(wù)執(zhí)行。如果執(zhí)行中遇到新的宏任務(wù)就分配到對(duì)應(yīng)的宏任務(wù)隊(duì)列中。遇到微任務(wù)就放到該宏任務(wù)的微任務(wù)隊(duì)列中。
- 一個(gè)宏任務(wù)執(zhí)行完畢后,執(zhí)行
process.nextTick
中的回調(diào)(如果有)。 - 執(zhí)行當(dāng)前宏任務(wù)的微任務(wù)隊(duì)列中的任務(wù),直到微任務(wù)隊(duì)列清空。
- 在上面的單個(gè)宏任務(wù)隊(duì)列中,再選擇一個(gè)宏任務(wù)執(zhí)行。直到當(dāng)前宏任務(wù)隊(duì)列清空或者到達(dá)上限。
- 選擇下一個(gè)宏任務(wù)隊(duì)列執(zhí)行。
6個(gè)宏任務(wù)隊(duì)列都執(zhí)行完畢,才叫做一次事件循環(huán)執(zhí)行完畢。
Node.js的11版本之前的區(qū)別
其中,在Node.js的11版本之前,宏任務(wù)和微任務(wù)的執(zhí)行關(guān)系與上述流程不同:
每個(gè)宏任務(wù)隊(duì)列有一個(gè)微任務(wù)隊(duì)列。在單個(gè)宏任務(wù)隊(duì)列中,首先執(zhí)行完所有的宏任務(wù),如果遇到微任務(wù)就放到微任務(wù)隊(duì)列中。當(dāng)單個(gè)宏任務(wù)隊(duì)列中的所有宏任務(wù)執(zhí)行完畢后,再執(zhí)行該宏任務(wù)隊(duì)列的微任務(wù)隊(duì)列。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-663299.html
對(duì)比執(zhí)行流程的區(qū)別,可以看到Node.js的11版本提高了微任務(wù)隊(duì)列中的優(yōu)先級(jí),讓Node.js中微任務(wù)隊(duì)列的優(yōu)先級(jí)和瀏覽器中的表現(xiàn)類似。而process.nextTick
可以看做是一個(gè)比微任務(wù)更高優(yōu)先級(jí)的鉤子。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-663299.html
注意
- setTimeout的時(shí)間即使設(shè)置為0,也會(huì)有一個(gè)最小時(shí)間,因此它與setImmediate誰(shuí)更早執(zhí)行不一定。
- 并不是所有回調(diào)函數(shù)都是異步的。例如
new Promise(fun)
中的回調(diào)是同步執(zhí)行,在回調(diào)中遇到resolve(), reject()
等才是微任務(wù)異步執(zhí)行的。
參考
- JavaScript 之事件循環(huán) (Event Loop)
https://xie.infoq.cn/article/921841837025748baac847030 - The Node.js Event Loop, Timers, and process.nextTick()
https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick
https://nodejs.org/zh-cn/docs/guides/event-loop-timers-and-nexttick - 深入理解瀏覽器中的進(jìn)程與線程
https://juejin.cn/post/6991849728493256741 - 這一篇瀏覽器事件循環(huán),可能會(huì)顛覆部分人的對(duì)宏任務(wù)和微任務(wù)的理解
https://juejin.cn/post/7259927532249710653 - HTML Living Standard (event-loops)
https://html.spec.whatwg.org/multipage/webappapis.html#event-loops - 阿里一面:熟悉事件循環(huán)?那談?wù)劄槭裁磿?huì)分為宏任務(wù)和微任務(wù)
https://juejin.cn/post/7073099307510923295 - node.js事件循環(huán)簡(jiǎn)單理解——定時(shí)器,process.nextTick()等
https://blog.csdn.net/qq_46561394/article/details/123172336 - 手摸手帶你徹底掌握,任務(wù)隊(duì)列、事件循環(huán)、宏任務(wù)、微任務(wù)
https://juejin.cn/post/6979876135182008357 - 瀏覽器UI線程和JS線程是同一個(gè)線程嗎?
https://www.zhihu.com/question/264253488 - 微信小程序的雙線程設(shè)計(jì)有何創(chuàng)新之處?瀏覽器的渲染線程和 JS 線程本來(lái)不就是兩個(gè)獨(dú)立線程嗎?
https://www.zhihu.com/question/446103629
到了這里,關(guān)于談一談瀏覽器與Node.js中的JavaScript事件循環(huán),宏任務(wù)與微任務(wù)機(jī)制的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!