Javascript 是一種單線(xiàn)程
的編程語(yǔ)言,只有一個(gè)調(diào)用棧,決定了它在同一時(shí)間只能做一件事。在代碼執(zhí)行的時(shí)候,通過(guò)將不同函數(shù)的執(zhí)行上下文壓入執(zhí)行棧中來(lái)保證代碼的有序執(zhí)行。在執(zhí)行同步代碼的時(shí)候,如果遇到了異步事件,js 引擎并不會(huì)一直等待其返回結(jié)果,而是會(huì)將這個(gè)事件掛起,繼續(xù)執(zhí)行執(zhí)行棧中的其他任務(wù)。因此JS又是一個(gè)非阻塞
、異步
、并發(fā)式
的編程語(yǔ)言。
進(jìn)程與線(xiàn)程的區(qū)別和聯(lián)系
當(dāng)我們啟動(dòng)某個(gè)程序時(shí),操作系統(tǒng)會(huì)給該程序創(chuàng)建一塊內(nèi)存,用來(lái)存放代碼、運(yùn)行中的數(shù)據(jù)和一個(gè)執(zhí)行任務(wù)的主線(xiàn)程,這樣的運(yùn)行環(huán)境就叫做進(jìn)程。
而線(xiàn)程是依附于進(jìn)程的,在進(jìn)程中使用多線(xiàn)程并行處理能提升運(yùn)算效率,進(jìn)程將任務(wù)分成很多細(xì)小的任務(wù),再創(chuàng)建多個(gè)線(xiàn)程,在里面并行分別執(zhí)行
進(jìn)程與進(jìn)程之間完全隔離,互不干擾,由于進(jìn)程之間是相互獨(dú)立的,所以一個(gè)進(jìn)程崩潰不會(huì)影響其他進(jìn)程,如瀏覽器每一個(gè)標(biāo)簽頁(yè)就是一個(gè)獨(dú)立的進(jìn)程,關(guān)閉其中一個(gè)標(biāo)簽頁(yè)別的標(biāo)簽頁(yè)并不會(huì)受到影響。
線(xiàn)程之間的數(shù)據(jù)是共享的,一個(gè)進(jìn)程可以有多個(gè)線(xiàn)程(一個(gè)進(jìn)程至少有一個(gè)線(xiàn)程),當(dāng)一個(gè)進(jìn)程有多個(gè)線(xiàn)程時(shí),每個(gè)線(xiàn)程都有一套獨(dú)立的寄存器和堆棧信息,而代碼、數(shù)據(jù)和文件是共享的
一個(gè)進(jìn)程中的任意一個(gè)線(xiàn)程執(zhí)行出錯(cuò),會(huì)導(dǎo)致這個(gè)進(jìn)程崩潰
當(dāng)一個(gè)進(jìn)程關(guān)閉之后,操作系統(tǒng)會(huì)回收該進(jìn)程的內(nèi)存空間
瀏覽器的進(jìn)程與線(xiàn)程
以大家熟悉的Chrome的內(nèi)核為例,他不僅是多線(xiàn)程的,而且是多進(jìn)程的。
最新的Chrome瀏覽器包括:瀏覽器主進(jìn)程,GPU進(jìn)程,網(wǎng)絡(luò)進(jìn)程,渲染進(jìn)程,和插件進(jìn)程
瀏覽器進(jìn)程
: 負(fù)責(zé)控制瀏覽器除標(biāo)簽頁(yè)外的界面,包括地址欄、書(shū)簽、前進(jìn)后退按鈕等,以及負(fù)責(zé)與其他進(jìn)程的協(xié)調(diào)工作,同時(shí)提供存儲(chǔ)功能
GPU進(jìn)程
:負(fù)責(zé)整個(gè)瀏覽器界面的渲染
網(wǎng)絡(luò)進(jìn)程
:負(fù)責(zé)發(fā)起和接受網(wǎng)絡(luò)請(qǐng)求
插件進(jìn)程
:主要是負(fù)責(zé)插件的運(yùn)行,因?yàn)椴寮赡鼙罎?,所以需要通過(guò)插件進(jìn)程來(lái)隔離,以保證插件崩潰也不會(huì)對(duì)瀏覽器和頁(yè)面造成影響
渲染進(jìn)程
:負(fù)責(zé)控制顯示tab標(biāo)簽頁(yè)內(nèi)的所有內(nèi)容,核心任務(wù)是將HTML、CSS、JS轉(zhuǎn)為用戶(hù)可以與之交互的網(wǎng)頁(yè),排版引擎Blink和JS引擎V8都是運(yùn)行在該進(jìn)程中,默認(rèn)情況下Chrome會(huì)為每個(gè)Tab標(biāo)簽頁(yè)創(chuàng)建一個(gè)渲染進(jìn)程
瀏覽器打開(kāi)一個(gè)頁(yè)面至少需要主進(jìn)程、GPU、網(wǎng)絡(luò)和渲染進(jìn)程,后續(xù)如果再打開(kāi)新的標(biāo)簽頁(yè)的話(huà),已經(jīng)創(chuàng)建好的瀏覽器進(jìn)程,GPU進(jìn)程,網(wǎng)絡(luò)進(jìn)程是共享的,不會(huì)重新啟動(dòng),默認(rèn)情況下會(huì)為每一個(gè)標(biāo)簽頁(yè)配置一個(gè)渲染進(jìn)程,但是也有例外,比如同一站點(diǎn)的頁(yè)面間跳轉(zhuǎn)就可能重用渲染進(jìn)程。
我們作為前端最關(guān)心的就是渲染進(jìn)程,那仔細(xì)來(lái)看一下渲染進(jìn)程。
渲染進(jìn)程
上面已經(jīng)提到渲染進(jìn)程負(fù)責(zé)控制顯示tab標(biāo)簽頁(yè)內(nèi)的所有內(nèi)容,核心任務(wù)是將HTML、CSS、JS轉(zhuǎn)為用戶(hù)可以與之交互的網(wǎng)頁(yè),排版引擎Blink和JS引擎V8都是運(yùn)行在該進(jìn)程中,默認(rèn)情況下Chrome會(huì)為每個(gè)Tab標(biāo)簽頁(yè)創(chuàng)建一個(gè)渲染進(jìn)程,某個(gè)選項(xiàng)卡崩潰,其他選項(xiàng)卡并不會(huì)受影響。
渲染進(jìn)程中的線(xiàn)程
GUI渲染線(xiàn)程
:GUI(圖形用戶(hù)界面),該線(xiàn)程負(fù)責(zé)渲染頁(yè)面,解析html和CSS、構(gòu)建DOM樹(shù)、CSSOM樹(shù)、渲染樹(shù)(包含要顯示的節(jié)點(diǎn)和節(jié)點(diǎn)的樣式信息,即整合 DOM 和 CSSOM 信息)、布局計(jì)算(計(jì)算節(jié)點(diǎn)在頁(yè)面的位置和大?。?、和繪制頁(yè)面(遍歷渲染樹(shù),調(diào)用 GPU 繪制,顯示在頁(yè)面上),重繪重排(回流)也是在該線(xiàn)程執(zhí)行,GUI更新會(huì)被保存在一個(gè)隊(duì)列中,等到JS引擎空閑時(shí),立即被執(zhí)行。
JS引擎線(xiàn)程:
一個(gè)tab頁(yè)中只有一個(gè)JS引擎線(xiàn)程(單線(xiàn)程),負(fù)責(zé)解析和執(zhí)行JS。這個(gè)線(xiàn)程就是負(fù)責(zé)執(zhí)行JS的主線(xiàn)程,"JS是單線(xiàn)程的"就是指的這個(gè)線(xiàn)程。大名鼎鼎的Chrome V8引擎就是在這個(gè)線(xiàn)程運(yùn)行的。需要注意的是,這個(gè)線(xiàn)程跟GUI線(xiàn)程是互斥的?;コ獾脑蚴荍S也可以操作DOM,如果JS線(xiàn)程和GUI線(xiàn)程同時(shí)操作DOM,結(jié)果就混亂了,不知道到底渲染哪個(gè)結(jié)果。這帶來(lái)的后果就是如果JS長(zhǎng)時(shí)間運(yùn)行,GUI線(xiàn)程就不能執(zhí)行,整個(gè)頁(yè)面就感覺(jué)卡死了。
計(jì)時(shí)器線(xiàn)程:
指setInterval和setTimeout,因?yàn)镴S引擎是單線(xiàn)程的,所以如果處于阻塞狀態(tài),那么計(jì)時(shí)器就會(huì)不準(zhǔn)了,所以需要單獨(dú)的線(xiàn)程來(lái)負(fù)責(zé)計(jì)時(shí)器工作。
異步http請(qǐng)求線(xiàn)程
:這個(gè)線(xiàn)程負(fù)責(zé)處理異步的ajax請(qǐng)求,當(dāng)請(qǐng)求完成后,他也會(huì)通知事件觸發(fā)線(xiàn)程,然后事件觸發(fā)線(xiàn)程將這個(gè)事件放入事件隊(duì)列給主線(xiàn)程執(zhí)行。
事件觸發(fā)線(xiàn)程:
定時(shí)器線(xiàn)程其實(shí)只是一個(gè)計(jì)時(shí)的作用,他并不會(huì)真正執(zhí)行時(shí)間到了的回調(diào),真正執(zhí)行這個(gè)回調(diào)的還是JS主線(xiàn)程。所以當(dāng)時(shí)間到了定時(shí)器線(xiàn)程會(huì)將這個(gè)回調(diào)事件給到事件觸發(fā)線(xiàn)程,然后事件觸發(fā)線(xiàn)程將它加到任務(wù)隊(duì)列里面去。最終JS主線(xiàn)程從任務(wù)隊(duì)列取出這個(gè)回調(diào)執(zhí)行。事件觸發(fā)線(xiàn)程管理著一個(gè)任務(wù)隊(duì)列,事件觸發(fā)線(xiàn)程不僅會(huì)將定時(shí)器事件放入任務(wù)隊(duì)列,其他滿(mǎn)足條件的事件也是他負(fù)責(zé)放進(jìn)任務(wù)隊(duì)列,如鼠標(biāo)點(diǎn)擊事件等。
setTimeout、DOM或者 HTTP請(qǐng)求這部分其實(shí)并不在 v8 引擎中,這些屬于webAPIs
,即瀏覽器的API,不是js引擎提供的。
所謂的事件循環(huán),或者說(shuō)js能夠?qū)崿F(xiàn)異步非阻塞特性的基礎(chǔ)就是因?yàn)槎嗑€(xiàn)程設(shè)計(jì)的存在。
消化總結(jié):
用戶(hù)啟動(dòng)某個(gè)應(yīng)用程序會(huì)建立一個(gè)或多個(gè)進(jìn)程,如瀏覽器的tab標(biāo)簽頁(yè),一個(gè)進(jìn)程中的任務(wù)被劃分到多個(gè)線(xiàn)程處理,有GUI渲染線(xiàn)程,JS引擎線(xiàn)程,網(wǎng)絡(luò)線(xiàn)程等,JS的單線(xiàn)程即是指瀏覽器渲染進(jìn)程中的JS引擎線(xiàn)程
(因?yàn)橹挥幸粋€(gè)JS引擎線(xiàn)程)。
了解了JS的單線(xiàn)程特性之后,我們來(lái)思考幾個(gè)問(wèn)題。
javascript為什么會(huì)是單線(xiàn)程的語(yǔ)言?
Javascript的單線(xiàn)程,與它的用途有關(guān)。作為瀏覽器腳本語(yǔ)言,Javascript的主要用途是與用戶(hù)互動(dòng),以及操作DOM。
在《javascript高級(jí)程序設(shè)計(jì)》一書(shū)中有一個(gè)很好的解釋?zhuān)喝绻鸍S是多線(xiàn)程語(yǔ)言,那么假如當(dāng)多個(gè)線(xiàn)程同時(shí)操作同一個(gè)DOM的時(shí)候,瀏覽器該如何渲染?瀏覽器該聽(tīng)哪個(gè)線(xiàn)程的指令?渲染結(jié)果是否會(huì)超出預(yù)期?基于這個(gè)特性,JS必須只能是單線(xiàn)程語(yǔ)言。
為了利用多核CPU的計(jì)算能力,HTML5提出Web Worker標(biāo)準(zhǔn),允許Javascript腳本創(chuàng)建多個(gè)線(xiàn)程,但是子線(xiàn)程完全受主線(xiàn)程控制,且不得操作DOM。所以,這個(gè)新標(biāo)準(zhǔn)并沒(méi)有改變Javascript單線(xiàn)程的本質(zhì)。
Javascript代碼是如何執(zhí)行的?
Javascript并不是一行一行的分析并執(zhí)行代碼的,所有的 JS 代碼在運(yùn)行時(shí)都是在執(zhí)行上下文中
進(jìn)行的。執(zhí)行上下文是一個(gè)抽象的概念,JS 中有三種執(zhí)行上下文:
全局執(zhí)行上下文
,默認(rèn)的,在瀏覽器中是 window 對(duì)象,并且 this 在非嚴(yán)格模式下指向它
函數(shù)執(zhí)行上下文
,JS 的函數(shù)每當(dāng)被調(diào)用時(shí)會(huì)創(chuàng)建一個(gè)上下文
Eval 執(zhí)行上下文
,eval 函數(shù)會(huì)產(chǎn)生自己的上下文,這里不討論
執(zhí)行上下文在執(zhí)行棧(調(diào)用棧)
中被以后進(jìn)先出的順序執(zhí)行。當(dāng)引擎第一次遇到 JS 代碼時(shí),會(huì)產(chǎn)生一個(gè)全局執(zhí)行上下文
并壓入執(zhí)行棧,每遇到一個(gè)函數(shù)調(diào)用,就會(huì)往棧中壓入一個(gè)新的函數(shù)執(zhí)行上下文
。引擎執(zhí)行棧頂
的函數(shù)(執(zhí)行上下文),執(zhí)行完畢,彈出當(dāng)前執(zhí)行上下文,并等待垃圾回收,全局上下文只有唯一的一個(gè),它在瀏覽器關(guān)閉時(shí)出棧。
棧,是一種數(shù)據(jù)結(jié)構(gòu),具有先進(jìn)后出的原則。JS 中的執(zhí)行棧就具有這樣的結(jié)構(gòu)。
遞歸函數(shù)對(duì)函數(shù)的每次遞歸調(diào)用都會(huì)創(chuàng)建一個(gè)新的執(zhí)行上下文,這意味著每次函數(shù)遞歸時(shí),都需要更多內(nèi)存來(lái)創(chuàng)建新上下文。
如何理解同步和異步?
同步任務(wù)
: 指的是在主線(xiàn)程上排隊(duì)執(zhí)行的任務(wù),只有前一個(gè)任務(wù)執(zhí)行完畢,才能執(zhí)行后一個(gè)任務(wù)。可以理解為在執(zhí)行完一個(gè)函數(shù)或方法之后,一直等待系統(tǒng)返回值或消息,這時(shí)程序是處于阻塞的,只有接收到返回的值或消息后才往下執(zhí)行其他的命令。
異步任務(wù)
:不進(jìn)入主線(xiàn)程、而進(jìn)入"任務(wù)隊(duì)列"(task queue)的任務(wù),只有"任務(wù)隊(duì)列"通知主線(xiàn)程,某個(gè)異步任務(wù)可以執(zhí)行了,該任務(wù)才會(huì)進(jìn)入主線(xiàn)程執(zhí)行。
舉個(gè)例子:你在燒水的時(shí)候還可以去洗菜切菜,因?yàn)闊阒恍枰蜷_(kāi)開(kāi)關(guān)然后等水自己燒開(kāi)提醒你就好了,不需要一直等著燒水什么都不做,這里的燒水就是異步任務(wù)。
為什么是異步、并發(fā)、非阻塞的?
我們?cè)陧?yè)面中通常會(huì)發(fā)大量的請(qǐng)求,獲取后端的數(shù)據(jù)去渲染頁(yè)面。因?yàn)闉g覽器是單線(xiàn)程的,試想一下,當(dāng)我們發(fā)出異步請(qǐng)求的時(shí)候,阻塞了,后面的代碼都不執(zhí)行了,那頁(yè)面可能出現(xiàn)長(zhǎng)時(shí)間白屏,極度影響用戶(hù)體驗(yàn)。
所以JS采取了"異步任務(wù)回調(diào)通知"的模式,而實(shí)現(xiàn)這個(gè)“通知”的,正是事件循環(huán),當(dāng)遇到異步任務(wù)時(shí),就將這個(gè)任務(wù)交給對(duì)應(yīng)的線(xiàn)程,當(dāng)這個(gè)異步任務(wù)滿(mǎn)足回調(diào)條件時(shí),對(duì)應(yīng)的線(xiàn)程又通過(guò)事件觸發(fā)線(xiàn)程將這個(gè)事件放入任務(wù)隊(duì)列,然后主線(xiàn)程從任務(wù)隊(duì)列取出事件繼續(xù)執(zhí)行。
事件循環(huán)并不是Javascript首創(chuàng)的,它是計(jì)算機(jī)的一種運(yùn)行機(jī)制。
基于JS的用途是瀏覽器腳本語(yǔ)言,用于操作DOM與用戶(hù)進(jìn)行交互,為了避免多個(gè)線(xiàn)程同時(shí)操作DOM導(dǎo)致渲染結(jié)果超出預(yù)期,所以JS被設(shè)計(jì)為一個(gè)單線(xiàn)程的語(yǔ)言。
開(kāi)發(fā)時(shí)會(huì)有很多耗時(shí)的異步任務(wù),如果都在主線(xiàn)程中阻塞,那會(huì)極度影響用戶(hù)體驗(yàn),所以JS是異步、并發(fā)、非阻塞的。
Javascript代碼的執(zhí)行過(guò)程中,依靠函數(shù)調(diào)用棧來(lái)搞定函數(shù)的執(zhí)行順序。
說(shuō)了這么多,終于輪到我們的主角了,下面有請(qǐng)任務(wù)隊(duì)列和事件循環(huán)登場(chǎng)。
任務(wù)隊(duì)列和事件循環(huán)
事件循環(huán)與任務(wù)隊(duì)列是JS中比較重要的兩個(gè)概念。這兩個(gè)概念在ES5和ES6兩個(gè)標(biāo)準(zhǔn)中有不同的實(shí)現(xiàn)。
ES5下的概念:
任務(wù)隊(duì)列是一個(gè)事件的隊(duì)列,所謂任務(wù)是WebAPIs返回的一個(gè)個(gè)通知,也可以理解成消息的隊(duì)列、回調(diào)隊(duì)列,里面存放異步任務(wù)的回調(diào),各個(gè)異步線(xiàn)程調(diào)用webAPI執(zhí)行完后通過(guò)事件觸發(fā)線(xiàn)程把回調(diào)函數(shù)放入任務(wù)隊(duì)列,表示相關(guān)的異步任務(wù)可以進(jìn)入“執(zhí)行棧”了,等待被主線(xiàn)程讀取。
瀏覽器包含3類(lèi)事件循環(huán):Window (用于運(yùn)行網(wǎng)頁(yè)內(nèi)容的瀏覽器級(jí)容器,包括實(shí)際的 window,一個(gè) tab 標(biāo)簽或者一個(gè) frame。)事件循環(huán)、Worker 事件循環(huán)、Worklet 事件循環(huán)
隊(duì)列里的每個(gè)任務(wù)都有一個(gè)任務(wù)源
(task source),源自同一個(gè)任務(wù)源的 task 必須放到同一個(gè)任務(wù)隊(duì)列,從不同源來(lái)的則被添加到不同隊(duì)列。
同一個(gè)任務(wù)隊(duì)列中的任務(wù)必須按先進(jìn)先出的順序執(zhí)行,但是不保證多個(gè)任務(wù)隊(duì)列中的任務(wù)優(yōu)先級(jí),具體實(shí)現(xiàn)可能會(huì)交叉執(zhí)行,進(jìn)入任務(wù)隊(duì)列的是他們指定的具體執(zhí)行任務(wù)(回調(diào)本身)。
setTimeout/Ajax/Promise/DOM事件(user interaction task source)
等都是任務(wù)源,來(lái)自同類(lèi)任務(wù)源的任務(wù)我們稱(chēng)它們是同源的,比如setTimeout與setInterval就是同源的。
ES5中的事件循環(huán),如圖:
圖中有三大塊:
函數(shù)調(diào)用棧:即執(zhí)行棧。
WebAPIs: 瀏覽器的接口,上面所說(shuō)的瀏覽器的對(duì)應(yīng)線(xiàn)程會(huì)使用這些接口處理,把它們放到相應(yīng)的任務(wù)隊(duì)列中。
任務(wù)隊(duì)列們: 主線(xiàn)程有多個(gè)任務(wù)隊(duì)列,同源的任務(wù)被放入在屬于自己的任務(wù)隊(duì)列。
"任務(wù)隊(duì)列"遵循先進(jìn)先出的原則,排在前面的事件,優(yōu)先被主線(xiàn)程讀取。主線(xiàn)程的讀取過(guò)程基本上是自動(dòng)的,只要執(zhí)行棧一清空,"任務(wù)隊(duì)列"上第一位的事件就自動(dòng)進(jìn)入主線(xiàn)程執(zhí)行。
主線(xiàn)程從"任務(wù)隊(duì)列"中讀取事件,這個(gè)過(guò)程是循環(huán)不斷的,所以整個(gè)的這種運(yùn)行機(jī)制又稱(chēng)為Event Loop(事件循環(huán))。
事件循環(huán)的大體流程:
主線(xiàn)程開(kāi)始執(zhí)行script代碼,同步代碼直接執(zhí)行,遇到異步任務(wù)源就將它掛起交給對(duì)應(yīng)的異步線(xiàn)程,自己繼續(xù)執(zhí)行同步任務(wù)
異步線(xiàn)程調(diào)用相應(yīng)API處理,滿(mǎn)足回調(diào)條件后,將異步回調(diào)事件放入任務(wù)隊(duì)列
主線(xiàn)程的執(zhí)行棧中的同步任務(wù)都執(zhí)行完畢后,就來(lái)讀取任務(wù)隊(duì)列中的異步任務(wù)回調(diào)事件
主線(xiàn)程不斷循環(huán)上述流程
到了ES6 的標(biāo)準(zhǔn),由于出現(xiàn)了 Promise ,ES5 時(shí)代的"同步任務(wù)"與"異步任務(wù)"已經(jīng)沒(méi)有辦法解釋其中的原理,因此出現(xiàn)了 task 隊(duì)列與 job 隊(duì)列之分。
ES6將任務(wù)分為 宏任務(wù)(macrotask)
與 微任務(wù)(microtask)
,在新ECMAscript標(biāo)準(zhǔn)中,它們被分別稱(chēng)為 task 與 jobs ;
任務(wù)隊(duì)列則為宏任務(wù)隊(duì)列
(Task Queue)和微任務(wù)隊(duì)列
(Job Queue)。
事件循環(huán)由宏任務(wù)和在執(zhí)行宏任務(wù)期間產(chǎn)生的所有微任務(wù)組成。宏任務(wù)隊(duì)列可以有多個(gè),微任務(wù)隊(duì)列只有一個(gè),完成當(dāng)下的宏任務(wù)后,會(huì)立刻執(zhí)行所有在此期間入隊(duì)的微任務(wù)。
這種設(shè)計(jì)是為了給緊急任務(wù)一個(gè)插隊(duì)的機(jī)會(huì),否則新入隊(duì)的任務(wù)永遠(yuǎn)被放在隊(duì)尾。微任務(wù)使得我們能夠在重新渲染UI之前執(zhí)行指定的行為,避免不必要的UI重繪。
TIPS: 其實(shí)并沒(méi)有宏任務(wù)隊(duì)列一說(shuō),人家原名就叫任務(wù)隊(duì)列(Task Queue)。首先要說(shuō)明宏任務(wù)其實(shí)一開(kāi)始就只是任務(wù)(task),因?yàn)镋S6新引入了Promise標(biāo)準(zhǔn),同時(shí)瀏覽器實(shí)現(xiàn)上多了一個(gè)microtask微任務(wù)概念,作為對(duì)照才稱(chēng)宏任務(wù),至于宏任務(wù)隊(duì)列,為了便于理解和區(qū)分大家就這么叫了。
宏任務(wù)(task)
進(jìn)入執(zhí)行棧等待主線(xiàn)程執(zhí)行的主代碼塊,包括從異步隊(duì)列里加入到棧的,如setTimeout()、setInterval()的回調(diào),其中不含異步隊(duì)列中的微任務(wù)如Promise.then回調(diào)。
宏任務(wù)大概包括:script(整塊代碼)
、setTimeout
、setInterval
、I/O
、DOM事件(UI交互事件)
、setImmediate
(node環(huán)境)、postMessage
、MessageChannel
,這些也被稱(chēng)作任務(wù)源
宏任務(wù)是瀏覽器規(guī)定的(W3C)
瀏覽器為了能夠使得JS內(nèi)部宏任務(wù)與DOM任務(wù)能夠有序的執(zhí)行,會(huì)在一個(gè)宏任務(wù)執(zhí)行結(jié)束后,在下一個(gè)宏任務(wù)執(zhí)行開(kāi)始前,對(duì)頁(yè)面進(jìn)行重新渲染(GUI線(xiàn)程接管渲染,更新DOM樹(shù),重新繪制)
異步任務(wù)可能是宏任務(wù)也可能是微任務(wù),而宏任務(wù)可能是異步代碼也可能是同步代碼,被掛起后放到任務(wù)隊(duì)列的是異步的宏任務(wù),同步宏任務(wù)會(huì)直接執(zhí)行
宏任務(wù)隊(duì)列可以有多個(gè),微任務(wù)隊(duì)列只有一個(gè)
Q:有很多小伙伴不理解為什么“script(整塊代碼)”是宏任務(wù)
A: MDN文檔定義中有詳細(xì)說(shuō)明。
一個(gè)任務(wù)就是指計(jì)劃由標(biāo)準(zhǔn)機(jī)制來(lái)執(zhí)行的任何 Javascript,如程序的初始化、事件觸發(fā)的回調(diào)等。 除了使用事件,你還可以使用 setTimeout() 或者 setInterval() 來(lái)添加任務(wù)。
由此可以得出結(jié)論,宏任務(wù)包含js主代碼塊,但是有一個(gè)爭(zhēng)議
存在,就是js主代碼塊是否進(jìn)入宏任務(wù)隊(duì)列中
,或者說(shuō)任務(wù)隊(duì)列是否只存放異步任務(wù)回調(diào)
關(guān)于這個(gè)問(wèn)題,目前主要存在兩種看法,
script(整塊代碼)是宏任務(wù)(同步),首先被放入宏任務(wù)隊(duì)列中,一個(gè)事件循環(huán)從宏任務(wù)隊(duì)列開(kāi)始,開(kāi)始執(zhí)行時(shí)宏任務(wù)隊(duì)列中只有script(整塊代碼)任務(wù),遇到同步代碼直接入執(zhí)行棧執(zhí)行,異步代碼放入對(duì)應(yīng)的任務(wù)隊(duì)列。
沒(méi)有把 script(整塊代碼)放入宏任務(wù)隊(duì)列,而是直接被主線(xiàn)程壓入執(zhí)行棧執(zhí)行,只有異步任務(wù)才會(huì)被掛起并放入任務(wù)隊(duì)列。
我個(gè)人其實(shí)更傾向于第二種說(shuō)法,因?yàn)閹缀跛形恼露贾赋鋈蝿?wù)隊(duì)列是消息隊(duì)列、回調(diào)隊(duì)列,我是實(shí)在沒(méi)有找到script(整塊代碼)是怎么被放入或者是以什么形式被放入任務(wù)隊(duì)列的相關(guān)說(shuō)明,但其實(shí)這兩種說(shuō)法在實(shí)際代碼運(yùn)行表現(xiàn)上都是一致的,所以你怎么理解并不影響后續(xù)的事件循環(huán)流程,大家如果找到更官方更明確的說(shuō)法歡迎交流,解惑。
微任務(wù)
可以理解是在當(dāng)前宏任務(wù)執(zhí)行結(jié)束后立即執(zhí)行的任務(wù)(宏任務(wù)的小跟班),也就是說(shuō),在當(dāng)前宏任務(wù)后,下一個(gè)宏任務(wù)之前,在重新渲染之前。
即宏任務(wù)->所有微任務(wù)->渲染,宏任務(wù)->所有微任務(wù)->渲染 ,...
微任務(wù)大概包括:new promise().then(回調(diào))
、MutationObserver(html5新特性)
、Object.observe(已廢棄,proxy替代)、process.nextTick(node環(huán)境)
,這些也被稱(chēng)作任務(wù)源
執(zhí)行宏任務(wù)的過(guò)程中如果遇到微任務(wù),就把微任務(wù)放到微任務(wù)隊(duì)列,這個(gè)過(guò)程由主線(xiàn)程維護(hù),而非事件觸發(fā)線(xiàn)程
當(dāng)執(zhí)行到script腳本的時(shí)候,js引擎會(huì)為全局創(chuàng)建一個(gè)執(zhí)行上下文,在該執(zhí)行上下文中維護(hù)了一個(gè)微任務(wù)隊(duì)列,這個(gè)微任務(wù)隊(duì)列是給 V8 引擎 內(nèi)部使用的,所以你是無(wú)法通過(guò) Javascript 直接訪問(wèn)的。
process.nextTick不在Event Loop的任何階段,他是一個(gè)特殊API,他會(huì)立即執(zhí)行,然后才會(huì)繼續(xù)執(zhí)行Event Loop,若同時(shí)存在promise和nextTick,則先執(zhí)行nextTick
區(qū)別:
任務(wù)隊(duì)列和微任務(wù)隊(duì)列的區(qū)別很簡(jiǎn)單,但卻很重要:
1.當(dāng)執(zhí)行來(lái)自任務(wù)隊(duì)列中的任務(wù)時(shí),在每一次新的事件循環(huán)開(kāi)始迭代的時(shí)候運(yùn)行時(shí)都會(huì)執(zhí)行隊(duì)列中的每個(gè)任務(wù)。在每次迭代開(kāi)始之后加入到隊(duì)列中的任務(wù)需要在下一次迭代開(kāi)始之后才會(huì)被執(zhí)行.
2.每次當(dāng)一個(gè)任務(wù)退出且執(zhí)行上下文為空的時(shí)候,微任務(wù)隊(duì)列中的每一個(gè)微任務(wù)會(huì)依次被執(zhí)行。不同的是它會(huì)等到微任務(wù)隊(duì)列為空才會(huì)停止執(zhí)行——即使中途有微任務(wù)加入。換句話(huà)說(shuō),微任務(wù)可以添加新的微任務(wù)到隊(duì)列中,并在下一個(gè)任務(wù)開(kāi)始執(zhí)行之前且當(dāng)前事件循環(huán)結(jié)束之前執(zhí)行完所有的微任務(wù)。
簡(jiǎn)單概括一下區(qū)別:
宏任務(wù)隊(duì)列一次循環(huán)執(zhí)行一個(gè)宏任務(wù),后面的宏任務(wù)下個(gè)循環(huán)執(zhí)行,微任務(wù)隊(duì)列一次循環(huán)執(zhí)行所有微任務(wù),即清空微任務(wù)隊(duì)列
微任務(wù)可以添加新的微任務(wù)到隊(duì)列中,中途插隊(duì)執(zhí)行
宏任務(wù)中的事件放在宏任務(wù)隊(duì)列中,由事件觸發(fā)線(xiàn)程維護(hù);微任務(wù)的事件放在微任務(wù)隊(duì)列中,由js引擎線(xiàn)程(主線(xiàn)程)維護(hù)
了解了宏任務(wù)和微任務(wù)的概念之后,我們來(lái)補(bǔ)充一下ES6事件循環(huán)的具體流程:
首先,javascript整體代碼被作為宏任務(wù)放入執(zhí)行棧中執(zhí)行,所有同步代碼先執(zhí)行,執(zhí)行過(guò)程中,當(dāng)遇到任務(wù)源時(shí),判斷是宏任務(wù)還是微任務(wù)
如果是宏任務(wù),加入到宏任務(wù)隊(duì)列中,如果是微任務(wù),加入到微任務(wù)隊(duì)列中
同步代碼執(zhí)行完成后,執(zhí)行棧空閑,檢查微任務(wù)隊(duì)列中是否有可執(zhí)行任務(wù),如果有,依次執(zhí)行微任務(wù)隊(duì)列中的所有任務(wù)
渲染UI,開(kāi)始下一輪循環(huán)
檢查宏任務(wù)隊(duì)列是否有可執(zhí)行的宏任務(wù),如果有,取出隊(duì)列中最前面的那個(gè)宏任務(wù),加入到執(zhí)行棧中開(kāi)始執(zhí)行,然后重復(fù)以上步驟,直到宏任務(wù)隊(duì)列中所有任務(wù)執(zhí)行結(jié)束
定時(shí)器不準(zhǔn)
任務(wù)隊(duì)列可以放置定時(shí)器回調(diào)事件,但是需要注意的是,setTimeout()只是將事件插入了"任務(wù)隊(duì)列",必須等到當(dāng)前代碼(執(zhí)行棧)執(zhí)行完,主線(xiàn)程才會(huì)去執(zhí)行它指定的回調(diào)函數(shù)。要是當(dāng)前代碼耗時(shí)很長(zhǎng),有可能要等很久,所以并沒(méi)有辦法保證,回調(diào)函數(shù)一定會(huì)在setTimeout()指定的時(shí)間執(zhí)行。
假設(shè)我們定義了一個(gè)2s的定時(shí)器,那么該定時(shí)器的執(zhí)行流程如下:
主線(xiàn)程執(zhí)行同步代碼
遇到setTimeout,將它交給定時(shí)器線(xiàn)程
定時(shí)器線(xiàn)程開(kāi)始計(jì)時(shí),2秒到了通知事件觸發(fā)線(xiàn)程
事件觸發(fā)線(xiàn)程將定時(shí)器回調(diào)放入事件隊(duì)列,異步流程到此結(jié)束
主線(xiàn)程如果有空,將定時(shí)器回調(diào)拿出來(lái)執(zhí)行,如果沒(méi)空這個(gè)回調(diào)就一直放在隊(duì)列里。
所以,如果在定義了定時(shí)器之后,我們又進(jìn)行了非常耗時(shí)的同步代碼運(yùn)算,那即使到了2s,同步代碼也會(huì)阻塞定時(shí)器回調(diào)事件的執(zhí)行,因此,此時(shí)回調(diào)執(zhí)行的時(shí)間必然是不準(zhǔn)確的了,所以再次強(qiáng)調(diào),寫(xiě)代碼時(shí)一定不要長(zhǎng)時(shí)間占用主線(xiàn)程。
事件循環(huán)總結(jié)
事件循環(huán)(Event Loop) 是讓 Javascript 做到既是單線(xiàn)程,又絕對(duì)不會(huì)阻塞的核心機(jī)制,也是 Javascript 并發(fā)模型的基礎(chǔ),是用來(lái)協(xié)調(diào)各種事件、用戶(hù)交互、腳本執(zhí)行、UI 渲染、網(wǎng)絡(luò)請(qǐng)求等的一種機(jī)制,具體的管理方法由它所處的運(yùn)行環(huán)境決定,目前JS的主要運(yùn)行環(huán)境有兩個(gè),瀏覽器和Node.js,這兩個(gè)環(huán)境的事件循環(huán)機(jī)制還有些區(qū)別,Node.js的事件循環(huán)我之后會(huì)另開(kāi)一篇文章細(xì)說(shuō)。
事件循環(huán)是讓 JS 做到既是單線(xiàn)程,又可以異步并發(fā)不會(huì)阻塞的核心機(jī)制。
瀏覽器是不僅是多進(jìn)程而且是多線(xiàn)程的,如渲染進(jìn)程中有GUI渲染線(xiàn)程、JS引擎線(xiàn)程、計(jì)時(shí)器線(xiàn)程、HTTP請(qǐng)求線(xiàn)程、事件觸發(fā)線(xiàn)程,事件循環(huán)就是依靠瀏覽器底層的多線(xiàn)程實(shí)現(xiàn),所謂JS的單線(xiàn)程指的就是瀏覽器渲染進(jìn)程中的JS引擎線(xiàn)程,因?yàn)橹挥幸粋€(gè)JS引擎線(xiàn)程,所以是單線(xiàn)程,也被稱(chēng)為主線(xiàn)程。
主線(xiàn)程執(zhí)行JS代碼的過(guò)程中,依靠執(zhí)行棧來(lái)管理執(zhí)行任務(wù)的順序,遵循后進(jìn)先出的原則,同步任務(wù)直接入棧執(zhí)行,異步任務(wù)被掛起待完成后被放入任務(wù)隊(duì)列,
任務(wù)隊(duì)列有宏任務(wù)隊(duì)列和微任務(wù)隊(duì)列的區(qū)別,宏任務(wù)隊(duì)列中存放宏任務(wù),如setTimeout、setInterval、DOM事件等,微任務(wù)隊(duì)列中存放微任務(wù),如Promise的then回調(diào)等。
當(dāng)執(zhí)行棧的任務(wù)執(zhí)行完成后會(huì)去讀取任務(wù)隊(duì)列中的任務(wù),優(yōu)先執(zhí)行微任務(wù)隊(duì)列中的所有任務(wù),微任務(wù)隊(duì)列清空后,重新渲染UI,開(kāi)始下一輪循環(huán),檢查宏任務(wù)隊(duì)列是否有可執(zhí)行的宏任務(wù),如果有,取出隊(duì)列中最前面的那個(gè)宏任務(wù),加入到執(zhí)行棧中開(kāi)始執(zhí)行,重復(fù)以上步驟就是事件循環(huán)。
參考文檔