進(jìn)程和線程
多線程可以并行處理任務(wù),但是線程是不能單獨(dú)存在的,它是由進(jìn)程來啟動(dòng)和管理的。
一個(gè)進(jìn)程就是一個(gè)程序的運(yùn)行實(shí)例。詳細(xì)解釋就是,啟動(dòng)一個(gè)程序的時(shí)候,操作系統(tǒng)會(huì)為該程序創(chuàng)建一塊內(nèi)存,用來存放代碼、運(yùn)行中的數(shù)據(jù)和一個(gè)執(zhí)行任務(wù)的主線程,我們把這樣的一個(gè)運(yùn)行環(huán)境叫進(jìn)程。

從圖中可以看到,線程是依附于進(jìn)程的,而進(jìn)程中使用多線程并行處理能提升運(yùn)算效率。
形象的比喻:
- 進(jìn)程是一個(gè)工廠,工廠有它的獨(dú)立資源
- 工廠之間相互獨(dú)立
- 線程是工廠中的工人,多個(gè)工人協(xié)作完成任務(wù)
- 工廠內(nèi)有一個(gè)或多個(gè)工人
- 工人之間共享空間
再完善完善概念:
- 工廠的資源 -> 系統(tǒng)分配的內(nèi)存(獨(dú)立的一塊內(nèi)存)
- 工廠之間的相互獨(dú)立 -> 進(jìn)程之間相互獨(dú)立
- 多個(gè)工人協(xié)作完成任務(wù) -> 多個(gè)線程在進(jìn)程中協(xié)作完成任務(wù)
- 工廠內(nèi)有一個(gè)或多個(gè)工人 -> 一個(gè)進(jìn)程由一個(gè)或多個(gè)線程組成
- 工人之間共享空間 -> 同一進(jìn)程下的各個(gè)線程之間共享程序的內(nèi)存空間(包括代碼段、數(shù)據(jù)集、堆等)
總結(jié)來說,進(jìn)程和線程之間的關(guān)系有以下 4 個(gè)特點(diǎn)。
- 進(jìn)程中的任意一線程執(zhí)行出錯(cuò),都會(huì)導(dǎo)致整個(gè)進(jìn)程的崩潰。
-
線程之間共享進(jìn)程中的數(shù)據(jù)。
線程之間可以對(duì)進(jìn)程的公共數(shù)據(jù)進(jìn)行讀寫操作。
線程之間共享進(jìn)程中的數(shù)據(jù)示意圖
從上圖可以看出,線程 1、線程 2、線程 3 分別把執(zhí)行的結(jié)果寫入 A、B、C 中,然后線程 2 繼續(xù)從 A、B、C 中讀取數(shù)據(jù),用來顯示執(zhí)行結(jié)果。
- 當(dāng)一個(gè)進(jìn)程關(guān)閉之后,操作系統(tǒng)會(huì)回收進(jìn)程所占用的內(nèi)存。
- 進(jìn)程之間的內(nèi)容相互隔離。
進(jìn)程隔離是為保護(hù)操作系統(tǒng)中進(jìn)程互不干擾的技術(shù),每一個(gè)進(jìn)程只能訪問自己占有的數(shù)據(jù),也就避免出現(xiàn)進(jìn)程 A 寫入數(shù)據(jù)到進(jìn)程 B 的情況。正是因?yàn)檫M(jìn)程之間的數(shù)據(jù)是嚴(yán)格隔離的,所以一個(gè)進(jìn)程如果崩潰了,或者掛起了,是不會(huì)影響到其他進(jìn)程的。如果進(jìn)程之間需要進(jìn)行數(shù)據(jù)的通信,這時(shí)候,就需要使用用于進(jìn)程間通信(IPC)的機(jī)制了。
多進(jìn)程瀏覽器時(shí)代
- 瀏覽器是多進(jìn)程的
- 瀏覽器之所以能夠運(yùn)行,是因?yàn)橄到y(tǒng)給它的進(jìn)程分配了資源(CPU、內(nèi)存)
- 簡(jiǎn)單點(diǎn)理解,每打開一個(gè)Tab頁,就相當(dāng)于創(chuàng)建了一個(gè)獨(dú)立的瀏覽器進(jìn)程
瀏覽器多進(jìn)程架構(gòu)

從圖中可以看出,Chrome 瀏覽器包括:1 個(gè)瀏覽器(Browser)主進(jìn)程、1 個(gè) GPU 進(jìn)程、1 個(gè)網(wǎng)絡(luò)(NetWork)進(jìn)程、多個(gè)渲染進(jìn)程和多個(gè)插件進(jìn)程。
這幾個(gè)進(jìn)程的功能:
- 瀏覽器主進(jìn)程:主要負(fù)責(zé)界面顯示、用戶交互、子進(jìn)程管理,同時(shí)提供存儲(chǔ)等功能。
- 插件進(jìn)程:主要是負(fù)責(zé)插件的運(yùn)行,因插件易崩潰,所以需要通過插件進(jìn)程來隔離,以保證插件進(jìn)程崩潰不會(huì)對(duì)瀏覽器和頁面造成影響。
- GPU進(jìn)程:用于3D CSS 的效果。
- 渲染進(jìn)程:核心任務(wù)是將HTML、CSS和JavaScript轉(zhuǎn)換為用戶可以與之交互的網(wǎng)頁,排版引擎Blink和JavaScript引擎V8都是運(yùn)行在該進(jìn)程中,默認(rèn)情況下,Chrome 會(huì)為每個(gè)Tab標(biāo)簽創(chuàng)建一個(gè)渲染進(jìn)程。出于安全考慮,渲染進(jìn)程都是運(yùn)行在沙箱模式下。
- 網(wǎng)絡(luò)進(jìn)程。主要負(fù)責(zé)頁面的網(wǎng)絡(luò)資源加載。
打開一個(gè)頁面需要啟動(dòng)多少進(jìn)程
打開1個(gè)頁面至少需要1個(gè)網(wǎng)絡(luò)進(jìn)程、1個(gè)瀏覽器進(jìn)程、1個(gè) GPU 進(jìn)程以及1個(gè)渲染進(jìn)程,共4個(gè);如果打開的頁面有運(yùn)行插件的話,還需要再加上1個(gè)插件進(jìn)程。
瀏覽器多進(jìn)程的優(yōu)勢(shì)
相比于單進(jìn)程瀏覽器,多進(jìn)程有如下優(yōu)點(diǎn):
- 避免單個(gè)
page crash影響整個(gè)瀏覽器 - 避免第三方插件
crash影響整個(gè)瀏覽器 - 多進(jìn)程充分利用多核優(yōu)勢(shì)
- 方便使用沙盒模型隔離插件等進(jìn)程,提高瀏覽器穩(wěn)定性
當(dāng)然,內(nèi)存等資源消耗也會(huì)更大,有點(diǎn)空間換時(shí)間的意思。
簡(jiǎn)單點(diǎn)理解:如果瀏覽器是單進(jìn)程,那么某個(gè)Tab頁崩潰了,就影響了整個(gè)瀏覽器;同理如果是單進(jìn)程,插件崩潰了也會(huì)影響整個(gè)瀏覽器。
重點(diǎn)是瀏覽器內(nèi)核(渲染進(jìn)程)
瀏覽器的渲染進(jìn)程是多線程的。
可以這樣理解,頁面的渲染,JS的執(zhí)行,事件的循環(huán),都在這個(gè)進(jìn)程內(nèi)進(jìn)行。
接下來列舉一些主要常駐線程:
GUI渲染線程
負(fù)責(zé)渲染瀏覽器界面,解析HTML,CSS,構(gòu)建DOM樹和Render樹,布局和繪制等。
當(dāng)界面需要重繪(Repaint)或由于某種操作引發(fā)回流(reflow)時(shí),該線程就會(huì)執(zhí)行。
注意,GUI渲染線程與JS引擎線程是互斥的,當(dāng)JS引擎執(zhí)行時(shí)GUI線程會(huì)被掛起(相當(dāng)于被凍結(jié)了),GUI更新會(huì)被保存在一個(gè)隊(duì)列中等到JS引擎空閑時(shí)立即被執(zhí)行。
JS引擎線程
也稱為JS內(nèi)核,負(fù)責(zé)處理Javascript腳本程序。(例如V8引擎)
JS引擎線程負(fù)責(zé)解析Javascript腳本,運(yùn)行代碼。
JS引擎一直等待著任務(wù)隊(duì)列中任務(wù)的到來,然后加以處理,一個(gè)Tab頁(renderer進(jìn)程)中無論什么時(shí)候都只有一個(gè)JS線程在運(yùn)行JS程序。
同樣注意,GUI渲染線程與JS引擎線程是互斥的,所以如果JS執(zhí)行的時(shí)間過長(zhǎng),這樣就會(huì)造成頁面的渲染不連貫,導(dǎo)致頁面渲染加載阻塞。
事件觸發(fā)線程
歸屬于瀏覽器而不是JS引擎,用來控制事件循環(huán)(可以理解,JS引擎自己都忙不過來,需要瀏覽器另開線程協(xié)助)。
當(dāng)JS引擎執(zhí)行代碼塊如setTimeout時(shí)(也可來自瀏覽器內(nèi)核的其他線程,如鼠標(biāo)點(diǎn)擊、AJAX異步請(qǐng)求等),會(huì)將對(duì)應(yīng)任務(wù)添加到事件線程中。
當(dāng)對(duì)應(yīng)的事件符合觸發(fā)條件被觸發(fā)時(shí),該線程會(huì)把事件添加到待處理隊(duì)列的隊(duì)尾,等待JS引擎的處理。
注意,由于JS的單線程關(guān)系,所以這些待處理隊(duì)列中的事件都得排隊(duì)等待JS引擎處理(當(dāng)JS引擎空閑時(shí)才會(huì)去執(zhí)行)。
定時(shí)觸發(fā)器線程
setInterval與setTimeout所在線程。
瀏覽器定時(shí)計(jì)數(shù)器并不是由JavaScript引擎計(jì)數(shù)的,(因?yàn)镴avaScript引擎是單線程的,如果處于阻塞線程狀態(tài)就會(huì)影響記計(jì)時(shí)的準(zhǔn)確)。
因此通過單獨(dú)線程來計(jì)時(shí)并觸發(fā)定時(shí)(計(jì)時(shí)完畢后,添加到事件隊(duì)列中,等待JS引擎空閑后執(zhí)行)。
注意,在HTML標(biāo)準(zhǔn)中規(guī)定,要求setTimeout中低于4ms的時(shí)間間隔算為4ms。
異步http請(qǐng)求線程
在XMLHttpRequest在連接后是通過瀏覽器新開一個(gè)線程請(qǐng)求。
將檢測(cè)到狀態(tài)變更時(shí),如果設(shè)置有回調(diào)函數(shù),異步線程就產(chǎn)生狀態(tài)變更事件,將這個(gè)回調(diào)再放入事件隊(duì)列中。再由JavaScript引擎執(zhí)行。

Browser進(jìn)程和瀏覽器內(nèi)核(Renderer進(jìn)程)的通信過程
打開任務(wù)管理器,然后打開一個(gè)瀏覽器,就可以看到:任務(wù)管理器中出現(xiàn)了兩個(gè)進(jìn)程(一個(gè)是主控進(jìn)程,一個(gè)則是打開Tab頁的渲染進(jìn)程),然后在這前提下,看下整個(gè)的過程:(簡(jiǎn)化了很多)
-
Browser進(jìn)程收到用戶請(qǐng)求,首先需要獲取頁面內(nèi)容(譬如通過網(wǎng)絡(luò)下載資源),隨后將該任務(wù)通過RendererHost接口傳遞給Render進(jìn)程 -
Renderer進(jìn)程的Renderer接口收到消息,簡(jiǎn)單解釋后,交給渲染線程,然后開始渲染- 渲染線程接收請(qǐng)求,加載網(wǎng)頁并渲染網(wǎng)頁,這其中可能需要
Browser進(jìn)程獲取資源和需要GPU進(jìn)程來幫助渲染 - 當(dāng)然可能會(huì)有JS線程操作DOM(這樣可能會(huì)造成回流并重繪)
- 最后
Render進(jìn)程將結(jié)果傳遞給Browser進(jìn)程
- 渲染線程接收請(qǐng)求,加載網(wǎng)頁并渲染網(wǎng)頁,這其中可能需要
-
Browser進(jìn)程接收到結(jié)果并將結(jié)果繪制出來

瀏覽器內(nèi)核中線程之間的關(guān)系
GUI渲染線程與JS引擎線程互斥
由于JavaScript是可操縱DOM的,如果在修改這些元素屬性同時(shí)渲染界面(即JS線程和UI線程同時(shí)運(yùn)行),那么渲染線程前后獲得的元素?cái)?shù)據(jù)就可能不一致了。
因此為了防止渲染出現(xiàn)不可預(yù)期的結(jié)果,瀏覽器設(shè)置GUI渲染線程與JS引擎為互斥的關(guān)系,當(dāng)JS引擎執(zhí)行時(shí)GUI線程會(huì)被掛起,GUI更新則會(huì)被保存在一個(gè)隊(duì)列中等到JS引擎線程空閑時(shí)立即被執(zhí)行。
JS阻塞頁面加載
從上述的互斥關(guān)系,可以推導(dǎo)出,JS如果執(zhí)行時(shí)間過長(zhǎng)就會(huì)阻塞頁面。
譬如,假設(shè)JS引擎正在進(jìn)行巨量的計(jì)算,此時(shí)就算GUI有更新,也會(huì)被保存到隊(duì)列中,等待JS引擎空閑后執(zhí)行。
然后,由于巨量計(jì)算,所以JS引擎很可能很久很久后才能空閑,自然會(huì)感覺到巨卡無比。
所以,要盡量避免JS執(zhí)行時(shí)間過長(zhǎng),這樣就會(huì)造成頁面的渲染不連貫,導(dǎo)致頁面渲染加載阻塞的感覺。
從Event Loop談JS的運(yùn)行機(jī)制
任務(wù)隊(duì)列
單線程就意味著,所有任務(wù)需要排隊(duì),前一個(gè)任務(wù)結(jié)束,才會(huì)執(zhí)行后一個(gè)任務(wù)。如果前一個(gè)任務(wù)耗時(shí)很長(zhǎng),后一個(gè)任務(wù)就必須一直等到前一個(gè)任務(wù)結(jié)束才能執(zhí)行。但這時(shí)CPU是閑著的,這樣浪費(fèi)了很多計(jì)算機(jī)的性能。
JavaScript語言的設(shè)計(jì)者意識(shí)到,這時(shí)主線程完全可以不管IO設(shè)備,掛起處于等待中的任務(wù),先運(yùn)行排在后面的任務(wù)。等到IO設(shè)備返回了結(jié)果,再回過頭,把掛起的任務(wù)繼續(xù)執(zhí)行下去。
于是,所有任務(wù)可以分成兩種,一種是同步任務(wù),另一種是異步任務(wù)。
同步任務(wù)指的是,在主線程上排隊(duì)執(zhí)行的任務(wù),只有前一個(gè)任務(wù)執(zhí)行完畢,才能執(zhí)行后一個(gè)任務(wù)。
異步任務(wù)指的是,不進(jìn)入主線程、而進(jìn)入"任務(wù)隊(duì)列"(task queue)的任務(wù),只有"任務(wù)隊(duì)列"通知主線程,某個(gè)異步任務(wù)可以執(zhí)行了,該任務(wù)才會(huì)進(jìn)入主線程執(zhí)行。
具體來說,異步執(zhí)行的運(yùn)行機(jī)制如下。(同步執(zhí)行也是如此,因?yàn)樗梢员灰暈闆]有異步任務(wù)的異步執(zhí)行。)
- 所有同步任務(wù)都在主線程上執(zhí)行,形成一個(gè)執(zhí)行棧。
- 主線程之外,事件觸發(fā)線程管理著一個(gè)"任務(wù)隊(duì)列"(
task queue)。只要異步任務(wù)有了運(yùn)行結(jié)果,就在"任務(wù)隊(duì)列"之中放置一個(gè)事件。 - 一旦"執(zhí)行棧"中的所有同步任務(wù)執(zhí)行完畢,系統(tǒng)就會(huì)讀取"任務(wù)隊(duì)列",看看里面有哪些事件。那些對(duì)應(yīng)的異步任務(wù),于是結(jié)束等待狀態(tài),進(jìn)入執(zhí)行棧,開始執(zhí)行。
- 主線程不斷重復(fù)上面的第三步。

我們不禁要問了,那怎么知道主線程執(zhí)行棧為空???js引擎存在monitoring process進(jìn)程,會(huì)持續(xù)不斷的檢查主線程執(zhí)行棧是否為空,一旦為空,就會(huì)去"任務(wù)隊(duì)列"那里檢查是否有等待被調(diào)用的函數(shù)。這就是JavaScript的運(yùn)行機(jī)制。這個(gè)過程會(huì)不斷重復(fù)。
事件和回調(diào)函數(shù)
"任務(wù)隊(duì)列"是一個(gè)事件的隊(duì)列(也可以理解成消息的隊(duì)列),IO設(shè)備完成一項(xiàng)任務(wù),就在"任務(wù)隊(duì)列"中添加一個(gè)事件,表示相關(guān)的異步任務(wù)可以進(jìn)入"執(zhí)行棧"了。主線程讀取"任務(wù)隊(duì)列",就是讀取里面有哪些事件。
"任務(wù)隊(duì)列"中的事件,除了IO設(shè)備的事件以外,還包括一些用戶產(chǎn)生的事件(比如鼠標(biāo)點(diǎn)擊、頁面滾動(dòng)等等)。只要指定過回調(diào)函數(shù),這些事件發(fā)生時(shí)就會(huì)進(jìn)入"任務(wù)隊(duì)列",等待主線程讀取。
所謂"回調(diào)函數(shù)"(callback),就是那些會(huì)被主線程掛起來的代碼。異步任務(wù)必須指定回調(diào)函數(shù),當(dāng)主線程開始執(zhí)行異步任務(wù),就是執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù)。
"任務(wù)隊(duì)列"是一個(gè)先進(jìn)先出的數(shù)據(jù)結(jié)構(gòu),排在前面的事件,優(yōu)先被主線程讀取。主線程的讀取過程基本上是自動(dòng)的,只要執(zhí)行棧一清空,"任務(wù)隊(duì)列"上第一位的事件就自動(dòng)進(jìn)入主線程。但是,由于存在后文提到的"定時(shí)器"功能,主線程首先要檢查一下執(zhí)行時(shí)間,某些事件只有到了規(guī)定的時(shí)間,才能返回主線程。
Event Loop
主線程從"任務(wù)隊(duì)列"中讀取事件,這個(gè)過程是循環(huán)不斷的,所以整個(gè)的這種運(yùn)行機(jī)制又稱為Event Loop(事件循環(huán))。

上圖中,主線程運(yùn)行的時(shí)候,產(chǎn)生堆(heap)和棧(stack),棧中的代碼調(diào)用各種外部API,它們?cè)?任務(wù)隊(duì)列"中加入各種事件(click,load,done)。只要棧中的代碼執(zhí)行完畢,主線程就會(huì)去讀取"任務(wù)隊(duì)列",依次執(zhí)行那些事件所對(duì)應(yīng)的回調(diào)函數(shù)。
執(zhí)行棧中的代碼(同步任務(wù)),總是在讀取"任務(wù)隊(duì)列"(異步任務(wù))之前執(zhí)行。
瀏覽器向我們提供了JS引擎不具備的特性:Web API。Web API包括DOM API、定時(shí)器、HTTP請(qǐng)求等特性,可以幫助我們實(shí)現(xiàn)異步、非阻塞的行為。
當(dāng)我們調(diào)用一個(gè)函數(shù)時(shí),函數(shù)會(huì)被放入一個(gè)叫做調(diào)用棧(call stack,也叫執(zhí)行上下文棧)的地方。調(diào)用棧是JS引擎的一部分,并非瀏覽器特有的。調(diào)用棧是一個(gè)棧數(shù)據(jù)結(jié)構(gòu),具有后進(jìn)先出的特點(diǎn)(Last in, first out. LIFO)。當(dāng)函數(shù)執(zhí)行完畢返回時(shí),會(huì)被彈出調(diào)用棧。

圖例中的respond函數(shù)返回一個(gè)setTimeout函數(shù)調(diào)用,setTimeout函數(shù)是Web API提供給我們的功能:它允許我們延遲執(zhí)行一個(gè)任務(wù)而不用阻塞主線程。setTimeout被調(diào)用時(shí),我們傳入的回調(diào)函數(shù),即箭頭函數(shù)()=>{return'hey'}會(huì)被傳遞給Web API處理,然后setTimeout和respond依次執(zhí)行完畢出棧。

在Web API中會(huì)執(zhí)行定時(shí)器,定時(shí)間隔就是我們傳入setTimeout的第二個(gè)參數(shù),也就是1000ms。計(jì)時(shí)結(jié)束后回調(diào)函數(shù)并不會(huì)立即進(jìn)入調(diào)用棧執(zhí)行,而是會(huì)被加入一個(gè)叫做 任務(wù)隊(duì)列(Task Queue)的地方。

Event Loop的工作就是連接任務(wù)隊(duì)列和調(diào)用棧,當(dāng)調(diào)用棧中的任務(wù)均執(zhí)行完畢出棧,調(diào)用棧為空時(shí),Event Loop會(huì)檢查任務(wù)隊(duì)列中是否存在等待執(zhí)行的任務(wù),如果存在,則取出隊(duì)列中第一個(gè)任務(wù),放入調(diào)用棧。

我們的回調(diào)函數(shù)被放入調(diào)用棧中,執(zhí)行完畢,返回其返回值,然后被彈出調(diào)用棧。

下面代碼輸出什么:
const foo = () => console.log('First');
const bar = () => setTimeout(() => console.log('Second'), 500);
const baz = () => console.log('Third');
bar();
foo();
baz();

-
bar被調(diào)用,返回setTimeout的調(diào)用; - 傳入
setTimeout的回調(diào)被傳遞給Web API處理,setTimeout執(zhí)行完畢出棧,bar執(zhí)行完畢出棧; - 定時(shí)器開始運(yùn)行,同時(shí)主線程中
foo被調(diào)用,打印First,foo執(zhí)行完畢出棧; -
baz被調(diào)用,打印Third,baz執(zhí)行完畢出棧; - 500ms后定時(shí)器運(yùn)行完畢,回調(diào)函數(shù)被放入任務(wù)隊(duì)列;
- Event Loop檢測(cè)到調(diào)用棧為空,從任務(wù)隊(duì)列中取出回調(diào)函數(shù)放入調(diào)用棧;
- 回調(diào)函數(shù)被執(zhí)行,打印
Second,執(zhí)行完畢出棧。
定時(shí)器
除了放置異步任務(wù)的事件,"任務(wù)隊(duì)列"還可以放置定時(shí)事件,即指定某些代碼在多少時(shí)間之后執(zhí)行。這叫做"定時(shí)器"功能,也就是定時(shí)執(zhí)行的代碼。
定時(shí)器功能主要由setTimeout()和setInterval()這兩個(gè)函數(shù)來完成,它們的內(nèi)部運(yùn)行機(jī)制完全一樣,區(qū)別在于前者指定的代碼是一次性執(zhí)行,后者則為反復(fù)執(zhí)行。setTimeout()接受兩個(gè)參數(shù),第一個(gè)是回調(diào)函數(shù),第二個(gè)是推遲執(zhí)行的毫秒數(shù)。
console.log(1);
setTimeout(function(){console.log(2);},1000);
console.log(3);
上面代碼的執(zhí)行結(jié)果是1,3,2。
如果將setTimeout()的第二個(gè)參數(shù)設(shè)為0,就表示當(dāng)前代碼執(zhí)行完(執(zhí)行棧清空)以后,立即執(zhí)行(0毫秒間隔)指定的回調(diào)函數(shù)。
setTimeout(function(){console.log(1);}, 0);
console.log(2);
總之,setTimeout(fn,0)的含義是,指定某個(gè)任務(wù)在主線程最早可得的空閑時(shí)間執(zhí)行,也就是說,盡可能早得執(zhí)行。它在"任務(wù)隊(duì)列"的尾部添加一個(gè)事件,因此要等到同步任務(wù)和"任務(wù)隊(duì)列"現(xiàn)有的事件都處理完,才會(huì)得到執(zhí)行。
H5標(biāo)準(zhǔn)規(guī)定了setTimeout()的第二個(gè)參數(shù)的最小值(最短間隔),不得低于4毫秒,如果低于這個(gè)值,就會(huì)自動(dòng)增加。在此之前,老版本的瀏覽器都將最短間隔設(shè)為10毫秒。另外,對(duì)于那些DOM的變動(dòng)(尤其是涉及頁面重新渲染的部分),通常不會(huì)立即執(zhí)行,而是每16毫秒執(zhí)行一次。這時(shí)使用requestAnimationFrame()的效果要好于setTimeout()。
需要注意的是,setTimeout()只是將事件插入了"任務(wù)隊(duì)列",必須等到當(dāng)前代碼(執(zhí)行棧)執(zhí)行完,主線程才會(huì)去執(zhí)行它指定的回調(diào)函數(shù)。要是當(dāng)前代碼耗時(shí)很長(zhǎng),有可能要等很久,所以并沒有辦法保證,回調(diào)函數(shù)一定會(huì)在setTimeout()指定的時(shí)間執(zhí)行。
宏任務(wù)與微任務(wù)
除了廣義的同步任務(wù)和異步任務(wù)之外,還有對(duì)任務(wù)更精細(xì)的劃分,分為:
macrotask(又稱之為宏任務(wù)),可以理解是每次執(zhí)行棧執(zhí)行的代碼就是一個(gè)宏任務(wù)(包括每次從事件隊(duì)列中獲取一個(gè)事件回調(diào)并放到執(zhí)行棧中執(zhí)行)
- 每一個(gè)
task會(huì)從頭到尾將這個(gè)任務(wù)執(zhí)行完畢,不會(huì)執(zhí)行其它 - 瀏覽器為了能夠使得JS內(nèi)部
task與DOM任務(wù)能夠有序的執(zhí)行,會(huì)在一個(gè)task執(zhí)行結(jié)束后,在下一個(gè)task執(zhí)行開始前,對(duì)頁面進(jìn)行重新渲染(task->渲染->task->...)
microtask(又稱為微任務(wù)),可以理解是在當(dāng)前task執(zhí)行結(jié)束后立即執(zhí)行的任務(wù)
- 也就是說,在當(dāng)前
task任務(wù)后,下一個(gè)task之前,在渲染之前 - 所以它的響應(yīng)速度相比
setTimeout(setTimeout是task)會(huì)更快,因?yàn)闊o需等渲染 - 也就是說,在某一個(gè)
macrotask執(zhí)行完后,就會(huì)將在它執(zhí)行期間產(chǎn)生的所有microtask都執(zhí)行完畢(在渲染前)
在ECMAScript中,microtask稱為jobs,macrotask可稱為task。
形成macrotask和microtask的場(chǎng)景:
-
macrotask:主代碼塊、setTimeout、setInterval -
microtask:Promise、process.nextTick
不同類型的任務(wù)會(huì)進(jìn)入對(duì)應(yīng)的任務(wù)隊(duì)列,比如setTimeout和setInterval會(huì)進(jìn)入相同的任務(wù)隊(duì)列。
事件循環(huán)的順序,決定js代碼的執(zhí)行順序。進(jìn)入整體代碼(宏任務(wù))后,開始第一次循環(huán)。接著執(zhí)行所有的微任務(wù)。然后再次從宏任務(wù)開始,找到其中一個(gè)任務(wù)隊(duì)列執(zhí)行完畢,再執(zhí)行所有的微任務(wù)。

所以js運(yùn)行過程:
- 執(zhí)行一個(gè)宏任務(wù)(棧中沒有就從事件隊(duì)列中獲?。?/li>
- 執(zhí)行過程中如果遇到微任務(wù),就將它添加到微任務(wù)的任務(wù)隊(duì)列中
- 宏任務(wù)執(zhí)行完畢后,立即執(zhí)行當(dāng)前微任務(wù)隊(duì)列中的所有微任務(wù)(依次執(zhí)行)
- 當(dāng)前宏任務(wù)執(zhí)行完畢,開始檢查渲染,然后GUI線程接管渲染
- 渲染完畢后,JS線程繼續(xù)接管,開始下一個(gè)宏任務(wù)(從事件隊(duì)列中獲取)
