「硬核JS」一次搞懂JS運行機制

以下文章來源于不正經(jīng)的前端 ,作者isboyjc

前言
本文首發(fā)于掘金,PC端點擊文章末尾閱讀原文觀看體驗更好

同時收錄于互聯(lián)小兵,技術(shù)人小程序,更多優(yōu)質(zhì)熱門文章可以前往體驗閱讀!

從開始做前端到目前為止,陸續(xù)看了很多帖子講JS運行機制,看過不久就忘了,還是自己理一遍好些

通過碼字使自己對JS運行機制相關(guān)內(nèi)容更加深刻(自己用心寫過的貼子,內(nèi)容也會牢記于心)

順道給大家看看(我太難了,深夜碼字,反復(fù)修改,說這么多就是想請你點個贊在看)

參考了很多資料(帖子),取其精華,去其糟糠,都在文末,可自行了解

是時候搞一波我大js了

從零到一百再到一,從多方面了解JS的運行機制,體會更深刻,請認真讀下去

本文大致分為以下這樣的步驟來幫助我們由廣入深更加清晰的了解JS運行機制

首先我們要了解進程和線程的概念

其次我們要知道瀏覽器的進程線程常識

再然后通過Event Loop、宏任務(wù)(macrotask)微任務(wù)(microtask)來看瀏覽器的幾個線程間是怎樣配合的

再然后通過例子來印證我們的猜想

最后提下NodeJS的運行機制

靈魂一問
JS運行機制在平常前端面試時不管是筆試題還是面試題命中率都極高

說到JS運行機制,你知道多少

看到這大家可能會說:JS運行機制嘛,很簡單,事件循環(huán)、宏微任務(wù)那點東西

是的,作為一名前端我們都了解,但是如果這真的面試問到了這個地方,你真的可以答好嗎(靈魂一問???)

不管你對JS了解多少,到這里大家不防先停止一下閱讀,假設(shè)你目前在面試,面試官讓你闡述下JS運行機制,思考下你的答案,用20秒的時間(面試時20s已經(jīng)很長了),然后帶著答案再接著往下看,有人曾經(jīng)說過:沒有思考的閱讀純粹是消磨時間罷了,這話很好(因為是我說的,皮一下??)

也有很多剛開始接觸JS的同學(xué)會被任務(wù)隊列 執(zhí)行棧 微任務(wù) 宏任務(wù)這些高大上點的名字搞的很懵

接下來,我們來細致的梳理一遍你就可以清晰的了解它們了

進程與線程
什么是進程
我們都知道,CPU是計算機的核心,承擔(dān)所有的計算任務(wù)

官網(wǎng)說法,進程是CPU資源分配的最小單位

字面意思就是進行中的程序,我將它理解為一個可以獨立運行且擁有自己的資源空間的任務(wù)程序

進程包括運行中的程序和程序所使用到的內(nèi)存和系統(tǒng)資源

CPU可以有很多進程,我們的電腦每打開一個軟件就會產(chǎn)生一個或多個進程,為什么電腦運行的軟件多就會卡,是因為CPU給每個進程分配資源空間,但是一個CPU一共就那么多資源,分出去越多,越卡,每個進程之間是相互獨立的,CPU在運行一個進程時,其他的進程處于非運行狀態(tài),CPU使用 時間片輪轉(zhuǎn)調(diào)度算法 來實現(xiàn)同時運行多個進程

什么是線程
線程是CPU調(diào)度的最小單位

線程是建立在進程的基礎(chǔ)上的一次程序運行單位,通俗點解釋線程就是程序中的一個執(zhí)行流,一個進程可以有多個線程

一個進程中只有一個執(zhí)行流稱作單線程,即程序執(zhí)行時,所走的程序路徑按照連續(xù)順序排下來,前面的必須處理好,后面的才會執(zhí)行

一個進程中有多個執(zhí)行流稱作多線程,即在一個程序中可以同時運行多個不同的線程來執(zhí)行不同的任務(wù),也就是說允許單個程序創(chuàng)建多個并行執(zhí)行的線程來完成各自的任務(wù)

進程和線程的區(qū)別
進程是操作系統(tǒng)分配資源的最小單位,線程是程序執(zhí)行的最小單位

一個進程由一個或多個線程組成,線程可以理解為是一個進程中代碼的不同執(zhí)行路線

進程之間相互獨立,但同一進程下的各個線程間共享程序的內(nèi)存空間(包括代碼段、數(shù)據(jù)集、堆等)及一些進程級的資源(如打開文件和信號)

調(diào)度和切換:線程上下文切換比進程上下文切換要快得多

多進程和多線程
多進程:多進程指的是在同一個時間里,同一個計算機系統(tǒng)中如果允許兩個或兩個以上的進程處于運行狀態(tài)。多進程帶來的好處是明顯的,比如大家可以在網(wǎng)易云聽歌的同時打開編輯器敲代碼,編輯器和網(wǎng)易云的進程之間不會相互干擾

多線程:多線程是指程序中包含多個執(zhí)行流,即在一個程序中可以同時運行多個不同的線程來執(zhí)行不同的任務(wù),也就是說允許單個程序創(chuàng)建多個并行執(zhí)行的線程來完成各自的任務(wù)

JS為什么是單線程
JS的單線程,與它的用途有關(guān)。作為瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很復(fù)雜的同步問題。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節(jié)點上添加內(nèi)容,另一個線程刪除了這個節(jié)點,這時瀏覽器應(yīng)該以哪個線程為準(zhǔn)?

還有人說js還有Worker線程,對的,為了利用多核CPU的計算能力,HTML5提出Web Worker標(biāo)準(zhǔn),允許JavaScript腳本創(chuàng)建多個線程,但是子線程是完 全受主線程控制的,而且不得操作DOM

所以,這個標(biāo)準(zhǔn)并沒有改變JavaScript是單線程的本質(zhì)

了解了進程和線程之后,接下來看看瀏覽器解析,瀏覽器之間也是有些許差距的,不過大致是差不多的,下文我們皆用市場占有比例最大的Chrome為例

瀏覽器
瀏覽器是多進程的
作為前端,免不了和瀏覽器打交道,瀏覽器是多進程的,拿Chrome來說,我們每打開一個Tab頁就會產(chǎn)生一個進程,我們使用Chrome打開很多標(biāo)簽頁不關(guān),電腦會越來越卡,不說其他,首先就很耗CPU

瀏覽器包含哪些進程
Browser進程

瀏覽器的主進程(負責(zé)協(xié)調(diào)、主控),該進程只有一個

負責(zé)瀏覽器界面顯示,與用戶交互。如前進,后退等

負責(zé)各個頁面的管理,創(chuàng)建和銷毀其他進程

將渲染(Renderer)進程得到的內(nèi)存中的Bitmap(位圖),繪制到用戶界面上

網(wǎng)絡(luò)資源的管理,下載等

第三方插件進程

每種類型的插件對應(yīng)一個進程,當(dāng)使用該插件時才創(chuàng)建

GPU進程

該進程也只有一個,用于3D繪制等等

渲染進程(重)

即通常所說的瀏覽器內(nèi)核(Renderer進程,內(nèi)部是多線程)

每個Tab頁面都有一個渲染進程,互不影響

主要作用為頁面渲染,腳本執(zhí)行,事件處理等

為什么瀏覽器要多進程
我們假設(shè)瀏覽器是單進程,那么某個Tab頁崩潰了,就影響了整個瀏覽器,體驗有多差

同理如果插件崩潰了也會影響整個瀏覽器

當(dāng)然多進程還有其它的諸多優(yōu)勢,不過多闡述

瀏覽器進程有很多,每個進程又有很多線程,都會占用內(nèi)存

這也意味著內(nèi)存等資源消耗會很大,有點拿空間換時間的意思

到此可不只是為了讓我們理解為何Chrome運行時間長了電腦會卡,哈哈,第一個重點來了

簡述渲染進程Renderer(重)
頁面的渲染,JS的執(zhí)行,事件的循環(huán),都在渲染進程內(nèi)執(zhí)行,所以我們要重點了解渲染進程

渲染進程是多線程的,我們來看渲染進程的一些常用較為主要的線程

渲染進程Renderer的主要線程
GUI渲染線程
負責(zé)渲染瀏覽器界面,解析HTML,CSS,構(gòu)建DOM樹和RenderObject樹,布局和繪制等

解析html代碼(HTML代碼本質(zhì)是字符串)轉(zhuǎn)化為瀏覽器認識的節(jié)點,生成DOM樹,也就是DOM Tree

解析css,生成CSSOM(CSS規(guī)則樹)

把DOM Tree 和CSSOM結(jié)合,生成Rendering Tree(渲染樹)

當(dāng)我們修改了一些元素的顏色或者背景色,頁面就會重繪(Repaint)

當(dāng)我們修改元素的尺寸,頁面就會回流(Reflow)

當(dāng)頁面需要Repaing和Reflow時GUI線程執(zhí)行,繪制頁面

回流(Reflow)比重繪(Repaint)的成本要高,我們要盡量避免Reflow和Repaint

GUI渲染線程與JS引擎線程是互斥的

當(dāng)JS引擎執(zhí)行時GUI線程會被掛起(相當(dāng)于被凍結(jié)了)

GUI更新會被保存在一個隊列中等到JS引擎空閑時立即被執(zhí)行

JS引擎線程
JS引擎線程就是JS內(nèi)核,負責(zé)處理Javascript腳本程序(例如V8引擎)

JS引擎線程負責(zé)解析Javascript腳本,運行代碼

JS引擎一直等待著任務(wù)隊列中任務(wù)的到來,然后加以處理

瀏覽器同時只能有一個JS引擎線程在運行JS程序,所以js是單線程運行的

一個Tab頁(renderer進程)中無論什么時候都只有一個JS線程在運行JS程序

GUI渲染線程與JS引擎線程是互斥的,js引擎線程會阻塞GUI渲染線程

就是我們常遇到的JS執(zhí)行時間過長,造成頁面的渲染不連貫,導(dǎo)致頁面渲染加載阻塞(就是加載慢)

例如瀏覽器渲染的時候遇到<script>標(biāo)簽,就會停止GUI的渲染,然后js引擎線程開始工作,執(zhí)行里面的js代碼,等js執(zhí)行完畢,js引擎線程停止工作,GUI繼續(xù)渲染下面的內(nèi)容。所以如果js執(zhí)行時間太長就會造成頁面卡頓的情況

事件觸發(fā)線程
屬于瀏覽器而不是JS引擎,用來控制事件循環(huán),并且管理著一個事件隊列(task queue)

當(dāng)js執(zhí)行碰到事件綁定和一些異步操作(如setTimeOut,也可來自瀏覽器內(nèi)核的其他線程,如鼠標(biāo)點擊、AJAX異步請求等),會走事件觸發(fā)線程將對應(yīng)的事件添加到對應(yīng)的線程中(比如定時器操作,便把定時器事件添加到定時器線程),等異步事件有了結(jié)果,便把他們的回調(diào)操作添加到事件隊列,等待js引擎線程空閑時來處理。

當(dāng)對應(yīng)的事件符合觸發(fā)條件被觸發(fā)時,該線程會把事件添加到待處理隊列的隊尾,等待JS引擎的處理

因為JS是單線程,所以這些待處理隊列中的事件都得排隊等待JS引擎處理

定時觸發(fā)器線程
setInterval與setTimeout所在線程

瀏覽器定時計數(shù)器并不是由JavaScript引擎計數(shù)的(因為JavaScript引擎是單線程的,如果處于阻塞線程狀態(tài)就會影響記計時的準(zhǔn)確)

通過單獨線程來計時并觸發(fā)定時(計時完畢后,添加到事件觸發(fā)線程的事件隊列中,等待JS引擎空閑后執(zhí)行),這個線程就是定時觸發(fā)器線程,也叫定時器線程

W3C在HTML標(biāo)準(zhǔn)中規(guī)定,規(guī)定要求setTimeout中低于4ms的時間間隔算為4ms

異步http請求線程
在XMLHttpRequest在連接后是通過瀏覽器新開一個線程請求

將檢測到狀態(tài)變更時,如果設(shè)置有回調(diào)函數(shù),異步線程就產(chǎn)生狀態(tài)變更事件,將這個回調(diào)再放入事件隊列中再由JavaScript引擎執(zhí)行

簡單說就是當(dāng)執(zhí)行到一個http異步請求時,就把異步請求事件添加到異步請求線程,等收到響應(yīng)(準(zhǔn)確來說應(yīng)該是http狀態(tài)變化),再把回調(diào)函數(shù)添加到事件隊列,等待js引擎線程來執(zhí)行

了解了上面這些基礎(chǔ)后,接下來我們開始進入今天的正題

事件循環(huán)(Event Loop)初探
首先要知道,JS分為同步任務(wù)和異步任務(wù)

同步任務(wù)都在主線程(這里的主線程就是JS引擎線程)上執(zhí)行,會形成一個執(zhí)行棧

主線程之外,事件觸發(fā)線程管理著一個任務(wù)隊列,只要異步任務(wù)有了運行結(jié)果,就在任務(wù)隊列之中放一個事件回調(diào)

一旦執(zhí)行棧中的所有同步任務(wù)執(zhí)行完畢(也就是JS引擎線程空閑了),系統(tǒng)就會讀取任務(wù)隊列,將可運行的異步任務(wù)(任務(wù)隊列中的事件回調(diào),只要任務(wù)隊列中有事件回調(diào),就說明可以執(zhí)行)添加到執(zhí)行棧中,開始執(zhí)行

我們來看一段簡單的代碼

let setTimeoutCallBack = function() {
  console.log('我是定時器回調(diào)');
};
let httpCallback = function() {
  console.log('我是http請求回調(diào)');
}

// 同步任務(wù)
console.log('我是同步任務(wù)1');

// 異步定時任務(wù)
setTimeout(setTimeoutCallBack,1000);

// 異步http請求任務(wù)
ajax.get('/info',httpCallback);

// 同步任務(wù)
console.log('我是同步任務(wù)2');

上述代碼執(zhí)行過程

JS是按照順序從上往下依次執(zhí)行的,可以先理解為這段代碼時的執(zhí)行環(huán)境就是主線程,也就是也就是當(dāng)前執(zhí)行棧

首先,執(zhí)行console.log('我是同步任務(wù)1')

接著,執(zhí)行到setTimeout時,會移交給定時器線程,通知定時器線程 1s 后將 setTimeoutCallBack 這個回調(diào)交給事件觸發(fā)線程處理,在 1s 后事件觸發(fā)線程會收到 setTimeoutCallBack 這個回調(diào)并把它加入到事件觸發(fā)線程所管理的事件隊列中等待執(zhí)行

接著,執(zhí)行http請求,會移交給異步http請求線程發(fā)送網(wǎng)絡(luò)請求,請求成功后將 httpCallback 這個回調(diào)交由事件觸發(fā)線程處理,事件觸發(fā)線程收到 httpCallback 這個回調(diào)后把它加入到事件觸發(fā)線程所管理的事件隊列中等待執(zhí)行






再接著執(zhí)行console.log('我是同步任務(wù)2')

至此主線程執(zhí)行棧中執(zhí)行完畢,JS引擎線程已經(jīng)空閑,開始向事件觸發(fā)線程發(fā)起詢問,詢問事件觸發(fā)線程的事件隊列中是否有需要執(zhí)行的回調(diào)函數(shù),如果有將事件隊列中的回調(diào)事件加入執(zhí)行棧中,開始執(zhí)行回調(diào),如果事件隊列中沒有回調(diào),JS引擎線程會一直發(fā)起詢問,直到有為止

到了這里我們發(fā)現(xiàn),瀏覽器上的所有線程的工作都很單一且獨立,非常符合單一原則

定時觸發(fā)線程只管理定時器且只關(guān)注定時不關(guān)心結(jié)果,定時結(jié)束就把回調(diào)扔給事件觸發(fā)線程

異步http請求線程只管理http請求同樣不關(guān)心結(jié)果,請求結(jié)束把回調(diào)扔給事件觸發(fā)線程

事件觸發(fā)線程只關(guān)心異步回調(diào)入事件隊列

而我們JS引擎線程只會執(zhí)行執(zhí)行棧中的事件,執(zhí)行棧中的代碼執(zhí)行完畢,就會讀取事件隊列中的事件并添加到執(zhí)行棧中繼續(xù)執(zhí)行,這樣反反復(fù)復(fù)就是我們所謂的事件循環(huán)(Event Loop)

圖解



首先,執(zhí)行棧開始順序執(zhí)行

判斷是否為同步,異步則進入異步線程,最終事件回調(diào)給事件觸發(fā)線程的任務(wù)隊列等待執(zhí)行,同步繼續(xù)執(zhí)行

執(zhí)行???,詢問任務(wù)隊列中是否有事件回調(diào)

任務(wù)隊列中有事件回調(diào)則把回調(diào)加入執(zhí)行棧末尾繼續(xù)從第一步開始執(zhí)行

任務(wù)隊列中沒有事件回調(diào)則不停發(fā)起詢問

宏任務(wù)(macrotask) & 微任務(wù)(microtask)
宏任務(wù)(macrotask)
在ECMAScript中,macrotask也被稱為task

我們可以將每次執(zhí)行棧執(zhí)行的代碼當(dāng)做是一個宏任務(wù)(包括每次從事件隊列中獲取一個事件回調(diào)并放到執(zhí)行棧中執(zhí)行), 每一個宏任務(wù)會從頭到尾執(zhí)行完畢,不會執(zhí)行其他

由于JS引擎線程和GUI渲染線程是互斥的關(guān)系,瀏覽器為了能夠使宏任務(wù)和DOM任務(wù)有序的進行,會在一個宏任務(wù)執(zhí)行結(jié)果后,在下一個宏任務(wù)執(zhí)行前,GUI渲染線程開始工作,對頁面進行渲染

宏任務(wù) -> GUI渲染 -> 宏任務(wù) -> ...

常見的宏任務(wù)

主代碼塊

setTimeout

setInterval

setImmediate ()-Node

requestAnimationFrame ()-瀏覽器

微任務(wù)(microtask)
ES6新引入了Promise標(biāo)準(zhǔn),同時瀏覽器實現(xiàn)上多了一個microtask微任務(wù)概念,在ECMAScript中,microtask也被稱為jobs

我們已經(jīng)知道宏任務(wù)結(jié)束后,會執(zhí)行渲染,然后執(zhí)行下一個宏任務(wù), 而微任務(wù)可以理解成在當(dāng)前宏任務(wù)執(zhí)行后立即執(zhí)行的任務(wù)

當(dāng)一個宏任務(wù)執(zhí)行完,會在渲染前,將執(zhí)行期間所產(chǎn)生的所有微任務(wù)都執(zhí)行完

宏任務(wù) -> 微任務(wù) -> GUI渲染 -> 宏任務(wù) -> ...

常見微任務(wù)

process.nextTick ()-Node

Promise.then()

catch

finally

Object.observe

MutationObserver

簡單區(qū)分宏任務(wù)與微任務(wù)
看了上述宏任務(wù)微任務(wù)的解釋你可能還不太清楚,沒關(guān)系,往下看,先記住那些常見的宏微任務(wù)即可

我們通過幾個例子來看,這幾個例子思路來自掘金云中君的文章參考鏈接【14】,通過渲染背景顏色來區(qū)分宏任務(wù)和微任務(wù),很直觀,我覺得很有意思,所以這里也用這種例子

找一個空白的頁面,在console中輸入以下代碼

document.body.style = 'background:black';
document.body.style = 'background:red';
document.body.style = 'background:blue';
document.body.style = 'background:pink';



我們看到上面動圖背景直接渲染了粉紅色,根據(jù)上文里講瀏覽器會先執(zhí)行完一個宏任務(wù),再執(zhí)行當(dāng)前執(zhí)行棧的所有微任務(wù),然后移交GUI渲染,上面四行代碼均屬于同一次宏任務(wù),全部執(zhí)行完才會執(zhí)行渲染,渲染時GUI線程會將所有UI改動優(yōu)化合并,所以視覺上,只會看到頁面變成粉紅色

再接著看

document.body.style = 'background:blue';
setTimeout(()=>{
    document.body.style = 'background:black'
},200)



上述代碼中,頁面會先卡一下藍色,再變成黑色背景,頁面上寫的是200毫秒,大家可以把它當(dāng)成0毫秒,因為0毫秒的話由于瀏覽器渲染太快,錄屏不好捕捉,我又沒啥錄屏慢放的工具,大家可以自行測試的,結(jié)果也是一樣,最安全的方法是寫一個index.html文件,在這個文件中插入上面的js腳本,然后瀏覽器打開,谷歌下使用控制臺中performance功能查看一幀一幀的加載最為恰當(dāng),不過這樣錄屏不好錄所以。。。

回歸正題,之所以會卡一下藍色,是因為以上代碼屬于兩次宏任務(wù),第一次宏任務(wù)執(zhí)行的代碼是將背景變成藍色,然后觸發(fā)渲染,將頁面變成藍色,再觸發(fā)第二次宏任務(wù)將背景變成黑色

再來看

document.body.style = 'background:blue'
console.log(1);
Promise.resolve().then(()=>{
    console.log(2);
    document.body.style = 'background:pink'
});
console.log(3);



控制臺輸出 1 3 2 , 是因為 promise 對象的 then 方法的回調(diào)函數(shù)是異步執(zhí)行,所以 2 最后輸出

頁面的背景色直接變成粉色,沒有經(jīng)過藍色的階段,是因為,我們在宏任務(wù)中將背景設(shè)置為藍色,但在進行渲染前執(zhí)行了微任務(wù), 在微任務(wù)中將背景變成了粉色,然后才執(zhí)行的渲染

微任務(wù)宏任務(wù)注意點
瀏覽器會先執(zhí)行一個宏任務(wù),緊接著執(zhí)行當(dāng)前執(zhí)行棧產(chǎn)生的微任務(wù),再進行渲染,然后再執(zhí)行下一個宏任務(wù)

微任務(wù)和宏任務(wù)不在一個任務(wù)隊列,不在一個任務(wù)隊列

例如setTimeout是一個宏任務(wù),它的事件回調(diào)在宏任務(wù)隊列,Promise.then()是一個微任務(wù),它的事件回調(diào)在微任務(wù)隊列,二者并不是一個任務(wù)隊列

以Chrome 為例,有關(guān)渲染的都是在渲染進程中執(zhí)行,渲染進程中的任務(wù)(DOM樹構(gòu)建,js解析…等等)需要主線程執(zhí)行的任務(wù)都會在主線程中執(zhí)行,而瀏覽器維護了一套事件循環(huán)機制,主線程上的任務(wù)都會放到消息隊列中執(zhí)行,主線程會循環(huán)消息隊列,并從頭部取出任務(wù)進行執(zhí)行,如果執(zhí)行過程中產(chǎn)生其他任務(wù)需要主線程執(zhí)行的,渲染進程中的其他線程會把該任務(wù)塞入到消息隊列的尾部,消息隊列中的任務(wù)都是宏任務(wù)

微任務(wù)是如何產(chǎn)生的呢?當(dāng)執(zhí)行到script腳本的時候,js引擎會為全局創(chuàng)建一個執(zhí)行上下文,在該執(zhí)行上下文中維護了一個微任務(wù)隊列,當(dāng)遇到微任務(wù),就會把微任務(wù)回調(diào)放在微隊列中,當(dāng)所有的js代碼執(zhí)行完畢,在退出全局上下文之前引擎會去檢查該隊列,有回調(diào)就執(zhí)行,沒有就退出執(zhí)行上下文,這也就是為什么微任務(wù)要早于宏任務(wù),也是大家常說的,每個宏任務(wù)都有一個微任務(wù)隊列(由于定時器是瀏覽器的API,所以定時器是宏任務(wù),在js中遇到定時器會也是放入到瀏覽器的隊列中)

此時,你可能還很迷惑,沒關(guān)系,請接著往下看

圖解宏任務(wù)和微任務(wù)



首先執(zhí)行一個宏任務(wù),執(zhí)行結(jié)束后判斷是否存在微任務(wù)

有微任務(wù)先執(zhí)行所有的微任務(wù),再渲染,沒有微任務(wù)則直接渲染

然后再接著執(zhí)行下一個宏任務(wù)






圖解完整的Event Loop



首先,整體的script(作為第一個宏任務(wù))開始執(zhí)行的時候,會把所有代碼分為同步任務(wù)、異步任務(wù)兩部分

同步任務(wù)會直接進入主線程依次執(zhí)行

異步任務(wù)會再分為宏任務(wù)和微任務(wù)

宏任務(wù)進入到Event Table中,并在里面注冊回調(diào)函數(shù),每當(dāng)指定的事件完成時,Event Table會將這個函數(shù)移到Event Queue中

微任務(wù)也會進入到另一個Event Table中,并在里面注冊回調(diào)函數(shù),每當(dāng)指定的事件完成時,Event Table會將這個函數(shù)移到Event Queue中

當(dāng)主線程內(nèi)的任務(wù)執(zhí)行完畢,主線程為空時,會檢查微任務(wù)的Event Queue,如果有任務(wù),就全部執(zhí)行,如果沒有就執(zhí)行下一個宏任務(wù)

上述過程會不斷重復(fù),這就是Event Loop,比較完整的事件循環(huán)

關(guān)于Promise
 new Promise(() => {}).then() ,我們來看這樣一個Promise代碼

前面的 new Promise() 這一部分是一個構(gòu)造函數(shù),這是一個同步任務(wù)

后面的 .then() 才是一個異步微任務(wù),這一點是非常重要的

new Promise((resolve) => {
  console.log(1)
  resolve()
}).then(()=>{
  console.log(2)
})
console.log(3)

上面代碼輸出1 3 2

關(guān)于 async/await 函數(shù)
async/await本質(zhì)上還是基于Promise的一些封裝,而Promise是屬于微任務(wù)的一種

所以在使用await關(guān)鍵字與Promise.then效果類似

setTimeout(() => console.log(4))

async function test() {
  console.log(1)
  await Promise.resolve()
  console.log(3)
}

test()

console.log(2)

上述代碼輸出1 2 3 4

可以理解為,await 以前的代碼,相當(dāng)于與 new Promise 的同步代碼,await 以后的代碼相當(dāng)于 Promise.then的異步

舉栗印證
我們在網(wǎng)上隨意找一個比較簡單的面試題,求輸出結(jié)果

function test() {
  console.log(1)
  setTimeout(function () {   // timer1
    console.log(2)
  }, 1000)
}

test();

setTimeout(function () {     // timer2
  console.log(3)
})

new Promise(function (resolve) {
  console.log(4)
  setTimeout(function () {   // timer3
    console.log(5)
  }, 100)
  resolve()
}).then(function () {
  setTimeout(function () {   // timer4
    console.log(6)
  }, 0)
  console.log(7)
})

console.log(8)

結(jié)合我們上述的JS運行機制再來看這道題就簡單明了的多了

JS是順序從上而下執(zhí)行

執(zhí)行到test(),test方法為同步,直接執(zhí)行,console.log(1)打印1

test方法中setTimeout為異步宏任務(wù),回調(diào)我們把它記做timer1放入宏任務(wù)隊列

接著執(zhí)行,test方法下面有一個setTimeout為異步宏任務(wù),回調(diào)我們把它記做timer2放入宏任務(wù)隊列

接著執(zhí)行promise,new Promise是同步任務(wù),直接執(zhí)行,打印4

new Promise里面的setTimeout是異步宏任務(wù),回調(diào)我們記做timer3放到宏任務(wù)隊列

Promise.then是微任務(wù),放到微任務(wù)隊列

console.log(8)是同步任務(wù),直接執(zhí)行,打印8

主線程任務(wù)執(zhí)行完畢,檢查微任務(wù)隊列中有Promise.then

開始執(zhí)行微任務(wù),發(fā)現(xiàn)有setTimeout是異步宏任務(wù),記做timer4放到宏任務(wù)隊列

微任務(wù)隊列中的console.log(7)是同步任務(wù),直接執(zhí)行,打印7

微任務(wù)執(zhí)行完畢,第一次循環(huán)結(jié)束

檢查宏任務(wù)隊列,里面有timer1、timer2、timer3、timer4,四個定時器宏任務(wù),按照定時器延遲時間得到可以執(zhí)行的順序,即Event Queue:timer2、timer4、timer3、timer1,依次拿出放入執(zhí)行棧末尾執(zhí)行(插播一條:瀏覽器 event loop 的 Macrotask queue,就是宏任務(wù)隊列在每次循環(huán)中只會讀取一個任務(wù))

執(zhí)行timer2,console.log(3)為同步任務(wù),直接執(zhí)行,打印3

檢查沒有微任務(wù),第二次Event Loop結(jié)束

執(zhí)行timer4,console.log(6)為同步任務(wù),直接執(zhí)行,打印6

檢查沒有微任務(wù),第三次Event Loop結(jié)束

執(zhí)行timer3,console.log(5)同步任務(wù),直接執(zhí)行,打印5

檢查沒有微任務(wù),第四次Event Loop結(jié)束

執(zhí)行timer1,console.log(2)同步任務(wù),直接執(zhí)行,打印2

檢查沒有微任務(wù),也沒有宏任務(wù),第五次Event Loop結(jié)束

結(jié)果:1,4,8,7,3,6,5,2

提一下NodeJS中的運行機制
上面的一切都是針對于瀏覽器的EventLoop

雖然NodeJS中的JavaScript運行環(huán)境也是V8,也是單線程,但是,還是有一些與瀏覽器中的表現(xiàn)是不一樣的

其實nodejs與瀏覽器的區(qū)別,就是nodejs的宏任務(wù)分好幾種類型,而這好幾種又有不同的任務(wù)隊列,而不同的任務(wù)隊列又有順序區(qū)別,而微任務(wù)是穿插在每一種宏任務(wù)之間的

在node環(huán)境下,process.nextTick的優(yōu)先級高于Promise,可以簡單理解為在宏任務(wù)結(jié)束后會先執(zhí)行微任務(wù)隊列中的nextTickQueue部分,然后才會執(zhí)行微任務(wù)中的Promise部分



上圖來自NodeJS官網(wǎng)

如上圖所示,nodejs的宏任務(wù)分好幾種類型,我們只簡單介紹大體內(nèi)容了解,不詳細解釋,不然又是啰哩啰嗦一大篇

NodeJS的Event Loop相對比較麻煩

Node會先執(zhí)行所有類型為 timers 的 MacroTask,然后執(zhí)行所有的 MicroTask(NextTick例外)

進入 poll 階段,執(zhí)行幾乎所有 MacroTask,然后執(zhí)行所有的 MicroTask

再執(zhí)行所有類型為 check 的 MacroTask,然后執(zhí)行所有的 MicroTask

再執(zhí)行所有類型為 close callbacks 的 MacroTask,然后執(zhí)行所有的 MicroTask

至此,完成一個 Tick,回到 timers 階段

……

如此反復(fù),無窮無盡……

反觀瀏覽器中Event Loop就比較容易理解

先執(zhí)行一個 MacroTask,然后執(zhí)行所有的 MicroTask

再執(zhí)行一個 MacroTask,然后執(zhí)行所有的 MicroTask

……

如此反復(fù),無窮無盡……

好了,關(guān)于Node中各個類型階段的解析,這里就不過多說明了,自己查閱資料吧,這里就是簡單提一下,NodeJS的Event Loop解釋起來比瀏覽器這繁雜,這里就只做個對比

最后
上面的流程圖都是自己畫的,所以有點low,見諒

水平有限,歡迎指錯

碼字不易,看完對你有幫助請點贊,有疑問請評論提出

參考

1.Tasks, microtasks, queues and schedules - 重點推薦閱讀https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/?utm_source=html5weekly

2.聊聊 JavaScript 與瀏覽器的那些事 - 引擎與線程https://zhuanlan.zhihu.com/p/32751855

3.前端文摘:深入解析瀏覽器的幕后工作原理https://www.cnblogs.com/lhb25/p/how-browsers-work.html

4.瀏覽器進程?線程?傻傻分不清楚!https://www.imweb.io/topic/58e3bfa845e5c13468f567d5

5.從輸入cnblogs.com到博客園首頁完全展示發(fā)生了什么 https://www.cnblogs.com/iovec/p/7904416.html

6.前端必讀:瀏覽器內(nèi)部工作原理https://www.cnblogs.com/wyaocn/p/5761163.html

7.什么是 Event Loop?http://www.ruanyifeng.com/blog/2013/10/event_loop.html

8.JavaScript 運行機制詳解:再談Event Loop http://www.ruanyifeng.com/blog/2014/10/event-loop.html

9.單線程與多線程的區(qū)別 https://blog.csdn.net/u012134199/article/details/46290465

10.瀏覽器進程/線程模型及JS運行機制 https://blog.csdn.net/qiuchangjun/article/details/79761242

11.瀏覽器的運行機制—2.瀏覽器都包含哪些進程? https://www.jianshu.com/p/1e455a9226ce

12.JS 一定要放在 Body 的最底部么?聊聊瀏覽器的渲染機制 https://segmentfault.com/a/1190000004292479

13.從瀏覽器多進程到JS單線程,JS運行機制最全面的一次梳理 https://juejin.im/post/5a6547d0f265da3e283a1df7#heading-25

14.「前端進階」從多線程到Event Loop全面梳理 https://juejin.im/post/5d5b4c2df265da03dd3d73e5#heading-4

15.Js基礎(chǔ)知識(四) - js運行原理與機制 https://segmentfault.com/a/1190000013119813

16.這一次,徹底弄懂 JavaScript 執(zhí)行機制 https://juejin.im/post/59e85eebf265da430d571f89

17.前端性能優(yōu)化:細說瀏覽器渲染的重排與重繪 https://juejin.im/post/5b543e26e51d4518f54404e4

18.10分鐘看懂瀏覽器的渲染過程及優(yōu)化 https://juejin.im/post/5d136700f265da1b7c6128db

作者:isboyjc



歡迎關(guān)注微信公眾號 :前端晚間課

更多文章,收錄于小程序-互聯(lián)網(wǎng)小兵