前言
從開始做前端到目前爲止,陸續看了很多帖子講JS運行機制,看過不久就忘了,還是自己理一遍好些
通過碼字使自己對JS運行機制相關內容更加深刻(自己用心寫過的貼子,內容也會牢記於心)
順道給大家看看(我太難了,深夜碼字,反覆修改,說這麼多就是想請你點個贊在看)
參考了很多資料(帖子),取其精華,去其糟糠,都在文末,可自行了解
是時候搞一波我大js了
從零到一百再到一,從多方面瞭解JS的運行機制,體會更深刻,請認真讀下去
本文大致分爲以下這樣的步驟來幫助我們由廣入深更加清晰的瞭解JS運行機制
- 首先我們要了解進程和線程的概念
- 其次我們要知道瀏覽器的進程線程常識
- 再然後通過Event Loop、宏任務(macrotask)微任務(microtask)來看瀏覽器的幾個線程間是怎樣配合的
- 再然後通過例子來印證我們的猜想
- 最後提下NodeJS的運行機制
靈魂一問
JS運行機制在平常前端面試時不管是筆試題還是面試題命中率都極高
說到JS運行機制,你知道多少
看到這大家可能回說:JS運行機制嘛,很簡單,事件循環、宏微任務那點東西
是的,作爲一名前端我們都瞭解,但是如果這真的面試問到了這個地方,你真的可以答好嗎(靈魂一問🤔️)
不管你對JS瞭解多少,到這裏大家不防先停止一下閱讀,假設你目前在面試,面試官讓你闡述下JS運行機制,思考下你的答案,用20秒的時間(面試時20s已經很長了),然後帶着答案再接着往下看,有人曾經說過:沒有思考的閱讀純粹是消磨時間罷了
,這話很好(因爲是我說的,皮一下😄)
也有很多剛開始接觸JS的同學會被任務隊列 執行棧 微任務 宏任務
這些高大上點的名次搞的很懵
接下來,我們來細緻的梳理一遍你就可以清晰的瞭解它們了
進程與線程
什麼是進程
我們都知道,CPU
是計算機的核心,承擔所有的計算任務
官網說法,進程
是CPU
資源分配的最小單位
字面意思就是進行中的程序,我將它理解爲一個可以獨立運行且擁有自己的資源空間的任務程序
進程
包括運行中的程序和程序所使用到的內存和系統資源
CPU
可以有很多進程,我們的電腦每打開一個軟件就會產生一個或多個進程
,爲什麼電腦運行的軟件多就會卡,是因爲CPU
給每個進程
分配資源空間,但是一個CPU
一共就那麼多資源,分出去越多,越卡,每個進程
之間是相互獨立的,CPU
在運行一個進程
時,其他的進程處於非運行狀態,CPU
使用 時間片輪轉調度算法 來實現同時運行多個進程
什麼是線程
線程
是CPU
調度的最小單位
線程
是建立在進程
的基礎上的一次程序運行單位,通俗點解釋線程
就是程序中的一個執行流,一個進程
可以有多個線程
一個進程
中只有一個執行流稱作單線程
,即程序執行時,所走的程序路徑按照連續順序排下來,前面的必須處理好,後面的纔會執行
一個進程
中有多個執行流稱作多線程
,即在一個程序中可以同時運行多個不同的線程
來執行不同的任務, 也就是說允許單個程序創建多個並行執行的線程
來完成各自的任務
進程和線程的區別
進程是操作系統分配資源的最小單位,線程是程序執行的最小單位
一個進程由一個或多個線程組成,線程可以理解爲是一個進程中代碼的不同執行路線
進程之間相互獨立,但同一進程下的各個線程間共享程序的內存空間(包括代碼段、數據集、堆等)及一些進程級的資源(如打開文件和信號)
調度和切換:線程上下文切換比進程上下文切換要快得多
多進程和多線程
多進程:多進程指的是在同一個時間裏,同一個計算機系統中如果允許兩個或兩個以上的進程處於運行狀態。多進程帶來的好處是明顯的,比如大家可以在網易雲聽歌的同時打開編輯器敲代碼,編輯器和網易雲的進程之間不會相互干擾
多線程:多線程是指程序中包含多個執行流,即在一個程序中可以同時運行多個不同的線程來執行不同的任務,也就是說允許單個程序創建多個並行執行的線程來完成各自的任務
JS爲什麼是單線程
JS的單線程,與它的用途有關。作爲瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很複雜的同步問題。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另一個線程刪除了這個節點,這時瀏覽器應該以哪個線程爲準?
還有人說js還有Worker線程,對的,爲了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript腳本創建多個線程,但是子線程是完 全受主線程控制的,而且不得操作DOM
所以,這個標準並沒有改變JavaScript是單線程的本質
瞭解了進程和線程之後,接下來看看瀏覽器解析,瀏覽器之間也是有些許差距的,不過大致是差不多的,下文我們皆用市場佔有比例最大的Chrome爲例
瀏覽器
瀏覽器是多進程的
作爲前端,免不了和瀏覽器打交道,瀏覽器是多進程的,拿Chrome來說,我們每打開一個Tab頁就會產生一個進程,我們使用Chrome打開很多標籤頁不關,電腦會越來越卡,不說其他,首先就很耗CPU
瀏覽器包含哪些進程
-
Browser進程
- 瀏覽器的主進程(負責協調、主控),該進程只有一個
- 負責瀏覽器界面顯示,與用戶交互。如前進,後退等
- 負責各個頁面的管理,創建和銷燬其他進程
- 將渲染(Renderer)進程得到的內存中的Bitmap(位圖),繪製到用戶界面上
- 網絡資源的管理,下載等
-
第三方插件進程
- 每種類型的插件對應一個進程,當使用該插件時才創建
-
GPU進程
- 該進程也只有一個,用於3D繪製等等
-
渲染進程(重)
- 即通常所說的瀏覽器內核(Renderer進程,內部是多線程)
- 每個Tab頁面都有一個渲染進程,互不影響
- 主要作用爲頁面渲染,腳本執行,事件處理等
爲什麼瀏覽器要多進程
我們假設瀏覽器是單進程,那麼某個Tab頁崩潰了,就影響了整個瀏覽器,體驗有多差
同理如果插件崩潰了也會影響整個瀏覽器
當然多進程還有其它的諸多優勢,不過多闡述
瀏覽器進程有很多,每個進程又有很多線程,都會佔用內存
這也意味着內存等資源消耗會很大,有點拿空間換時間的意思
到此可不只是爲了讓我們理解爲何Chrome運行時間長了電腦會卡,哈哈,第一個重點來了
簡述渲染進程Renderer(重)
頁面的渲染,JS的執行,事件的循環,都在渲染進程內執行,所以我們要重點了解渲染進程
渲染進程是多線程的,我們來看渲染進程的一些常用較爲主要的線程
渲染進程Renderer的主要線程
GUI渲染線程
- 負責渲染瀏覽器界面,解析HTML,CSS,構建DOM樹和RenderObject樹,佈局和繪製等
- 解析html代碼(HTML代碼本質是字符串)轉化爲瀏覽器認識的節點,生成DOM樹,也就是DOM Tree
- 解析css,生成CSSOM(CSS規則樹)
- 把DOM Tree 和CSSOM結合,生成Rendering Tree(渲染樹)
- 當我們修改了一些元素的顏色或者背景色,頁面就會重繪(Repaint)
- 當我們修改元素的尺寸,頁面就會迴流(Reflow)
- 當頁面需要Repaing和Reflow時GUI線程執行,繪製頁面
- 迴流(Reflow)比重繪(Repaint)的成本要高,我們要儘量避免Reflow和Repaint
- GUI渲染線程與JS引擎線程是互斥的
- 當JS引擎執行時GUI線程會被掛起(相當於被凍結了)
- GUI更新會被保存在一個隊列中等到JS引擎空閒時立即被執行
JS引擎線程
- JS引擎線程就是JS內核,負責處理Javascript腳本程序(例如V8引擎)
- JS引擎線程負責解析Javascript腳本,運行代碼
- JS引擎一直等待着任務隊列中任務的到來,然後加以處理
- 瀏覽器同時只能有一個JS引擎線程在運行JS程序,所以js是單線程運行的
- 一個Tab頁(renderer進程)中無論什麼時候都只有一個JS線程在運行JS程序
- GUI渲染線程與JS引擎線程是互斥的,js引擎線程會阻塞GUI渲染線程
- 就是我們常遇到的JS執行時間過長,造成頁面的渲染不連貫,導致頁面渲染加載阻塞(就是加載慢)
- 例如瀏覽器渲染的時候遇到
<script>
標籤,就會停止GUI的渲染,然後js引擎線程開始工作,執行裏面的js代碼,等js執行完畢,js引擎線程停止工作,GUI繼續渲染下面的內容。所以如果js執行時間太長就會造成頁面卡頓的情況
事件觸發線程
- 屬於瀏覽器而不是JS引擎,用來控制事件循環,並且管理着一個事件隊列(task queue)
- 當js執行碰到事件綁定和一些異步操作(如setTimeOut,也可來自瀏覽器內核的其他線程,如鼠標點擊、AJAX異步請求等),會走事件觸發線程將對應的事件添加到對應的線程中(比如定時器操作,便把定時器事件添加到定時器線程),等異步事件有了結果,便把他們的回調操作添加到事件隊列,等待js引擎線程空閒時來處理。
- 當對應的事件符合觸發條件被觸發時,該線程會把事件添加到待處理隊列的隊尾,等待JS引擎的處理
- 因爲JS是單線程,所以這些待處理隊列中的事件都得排隊等待JS引擎處理
定時觸發器線程
setInterval
與setTimeout
所在線程- 瀏覽器定時計數器並不是由JavaScript引擎計數的(因爲JavaScript引擎是單線程的,如果處於阻塞線程狀態就會影響記計時的準確)
- 通過單獨線程來計時並觸發定時(計時完畢後,添加到事件觸發線程的事件隊列中,等待JS引擎空閒後執行),這個線程就是定時觸發器線程,也叫定時器線程
- W3C在HTML標準中規定,規定要求
setTimeout
中低於4ms的時間間隔算爲4ms
異步http請求線程
- 在XMLHttpRequest在連接後是通過瀏覽器新開一個線程請求
- 將檢測到狀態變更時,如果設置有回調函數,異步線程就產生狀態變更事件,將這個回調再放入事件隊列中再由JavaScript引擎執行
- 簡單說就是當執行到一個http異步請求時,就把異步請求事件添加到異步請求線程,等收到響應(準確來說應該是http狀態變化),再把回調函數添加到事件隊列,等待js引擎線程來執行
瞭解了上面這些基礎後,接下來我們開始進入今天的正題
事件循環(Event Loop)初探
首先要知道,JS分爲同步任務和異步任務
同步任務都在主線程(這裏的主線程就是JS引擎線程)上執行,會形成一個執行棧
主線程之外,事件觸發線程管理着一個任務隊列
,只要異步任務有了運行結果,就在任務隊列
之中放一個事件回調
一旦執行棧
中的所有同步任務執行完畢(也就是JS引擎線程空閒了),系統就會讀取任務隊列
,將可運行的異步任務(任務隊列中的事件回調,只要任務隊列中有事件回調,就說明可以執行)添加到執行棧中,開始執行
我們來看一段簡單的代碼
let setTimeoutCallBack = function() {
console.log('我是定時器回調');
};
let httpCallback = function() {
console.log('我是http請求回調');
}
// 同步任務
console.log('我是同步任務1');
// 異步定時任務
setTimeout(setTimeoutCallBack,1000);
// 異步http請求任務
ajax.get('/info',httpCallback);
// 同步任務
console.log('我是同步任務2');
複製代碼
上述代碼執行過程
JS是按照順序從上往下依次執行的,可以先理解爲這段代碼時的執行環境就是主線程,也就是也就是當前執行棧
首先,執行console.log('我是同步任務1')
接着,執行到setTimeout
時,會移交給定時器線程
,通知定時器線程
1s 後將 setTimeoutCallBack
這個回調交給事件觸發線程
處理,在 1s 後事件觸發線程
會收到 setTimeoutCallBack
這個回調並把它加入到事件觸發線程
所管理的事件隊列中等待執行
接着,執行http請求,會移交給異步http請求線程
發送網絡請求,請求成功後將 httpCallback
這個回調交由事件觸發線程處理,事件觸發線程
收到 httpCallback
這個回調後把它加入到事件觸發線程
所管理的事件隊列中等待執行
再接着執行console.log('我是同步任務2')
至此主線程執行棧中執行完畢,JS引擎線程
已經空閒,開始向事件觸發線程
發起詢問,詢問事件觸發線程
的事件隊列中是否有需要執行的回調函數,如果有將事件隊列中的回調事件加入執行棧中,開始執行回調,如果事件隊列中沒有回調,JS引擎線程
會一直髮起詢問,直到有爲止
到了這裏我們發現,瀏覽器上的所有線程的工作都很單一且獨立,非常符合單一原則
定時觸發線程只管理定時器且只關注定時不關心結果,定時結束就把回調扔給事件觸發線程
異步http請求線程只管理http請求同樣不關心結果,請求結束把回調扔給事件觸發線程
事件觸發線程只關心異步回調入事件隊列
而我們JS引擎線程只會執行執行棧中的事件,執行棧中的代碼執行完畢,就會讀取事件隊列中的事件並添加到執行棧中繼續執行,這樣反反覆覆就是我們所謂的事件循環(Event Loop)
圖解
首先,執行棧開始順序執行
判斷是否爲同步,異步則進入異步線程,最終事件回調給事件觸發線程的任務隊列等待執行,同步繼續執行
執行棧空,詢問任務隊列中是否有事件回調
任務隊列中有事件回調則把回調加入執行棧末尾繼續從第一步開始執行
任務隊列中沒有事件回調則不停發起詢問
宏任務(macrotask) & 微任務(microtask)
宏任務(macrotask)
在ECMAScript中,macrotask
也被稱爲task
我們可以將每次執行棧執行的代碼當做是一個宏任務(包括每次從事件隊列中獲取一個事件回調並放到執行棧中執行), 每一個宏任務會從頭到尾執行完畢,不會執行其他
由於JS引擎線程
和GUI渲染線程
是互斥的關係,瀏覽器爲了能夠使宏任務
和DOM任務
有序的進行,會在一個宏任務
執行結果後,在下一個宏任務
執行前,GUI渲染線程
開始工作,對頁面進行渲染
宏任務 -> GUI渲染 -> 宏任務 -> ...
複製代碼
常見的宏任務
- 主代碼塊
- setTimeout
- setInterval
- setImmediate ()-Node
- requestAnimationFrame ()-瀏覽器
微任務(microtask)
ES6新引入了Promise標準,同時瀏覽器實現上多了一個microtask
微任務概念,在ECMAScript中,microtask
也被稱爲jobs
我們已經知道宏任務
結束後,會執行渲染,然後執行下一個宏任務
, 而微任務可以理解成在當前宏任務
執行後立即執行的任務
當一個宏任務
執行完,會在渲染前,將執行期間所產生的所有微任務
都執行完
宏任務 -> 微任務 -> GUI渲染 -> 宏任務 -> ...
複製代碼
常見微任務
- process.nextTick ()-Node
- Promise.then()
- catch
- finally
- Object.observe
- MutationObserver
簡單區分宏任務與微任務
看了上述宏任務微任務的解釋你可能還不太清楚,沒關係,往下看,先記住那些常見的宏微任務即可
我們通過幾個例子來看,這幾個例子思路來自掘金雲中君
的文章參考鏈接【14】,通過渲染背景顏色來區分宏任務和微任務,很直觀,我覺得很有意思,所以這裏也用這種例子
找一個空白的頁面,在console中輸入以下代碼
document.body.style = 'background:black';
document.body.style = 'background:red';
document.body.style = 'background:blue';
document.body.style = 'background:pink';
複製代碼
我們看到上面動圖背景直接渲染了粉紅色,根據上文裏講瀏覽器會先執行完一個宏任務,再執行當前執行棧的所有微任務,然後移交GUI渲染,上面四行代碼均屬於同一次宏任務,全部執行完纔會執行渲染,渲染時GUI線程
會將所有UI改動優化合並,所以視覺上,只會看到頁面變成粉紅色
再接着看
document.body.style = 'background:blue';
setTimeout(()=>{
document.body.style = 'background:black'
},200)
複製代碼
上述代碼中,頁面會先卡一下藍色,再變成黑色背景,頁面上寫的是200毫秒,大家可以把它當成0毫秒,因爲0毫秒的話由於瀏覽器渲染太快,錄屏不好捕捉,我又沒啥錄屏慢放的工具,大家可以自行測試的,結果也是一樣,最安全的方法是寫一個index.html
文件,在這個文件中插入上面的js腳本,然後瀏覽器打開,谷歌下使用控制檯中performance
功能查看一幀一幀的加載最爲恰當,不過這樣錄屏不好錄所以。。。
迴歸正題,之所以會卡一下藍色,是因爲以上代碼屬於兩次宏任務
,第一次宏任務
執行的代碼是將背景變成藍色,然後觸發渲染,將頁面變成藍色,再觸發第二次宏任務將背景變成黑色
再來看
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 方法的回調函數是異步執行,所以 2 最後輸出
頁面的背景色直接變成粉色,沒有經過藍色的階段,是因爲,我們在宏任務中將背景設置爲藍色,但在進行渲染前執行了微任務, 在微任務中將背景變成了粉色,然後才執行的渲染
微任務宏任務注意點
- 瀏覽器會先執行一個宏任務,緊接着執行當前執行棧產生的微任務,再進行渲染,然後再執行下一個宏任務
- 微任務和宏任務不在一個任務隊列,不在一個任務隊列
- 例如
setTimeout
是一個宏任務,它的事件回調在宏任務隊列,Promise.then()
是一個微任務,它的事件回調在微任務隊列,二者並不是一個任務隊列 - 以Chrome 爲例,有關渲染的都是在渲染進程中執行,渲染進程中的任務(DOM樹構建,js解析…等等)需要主線程執行的任務都會在主線程中執行,而瀏覽器維護了一套事件循環機制,主線程上的任務都會放到消息隊列中執行,主線程會循環消息隊列,並從頭部取出任務進行執行,如果執行過程中產生其他任務需要主線程執行的,渲染進程中的其他線程會把該任務塞入到消息隊列的尾部,消息隊列中的任務都是宏任務
- 微任務是如何產生的呢?當執行到script腳本的時候,js引擎會爲全局創建一個執行上下文,在該執行上下文中維護了一個微任務隊列,當遇到微任務,就會把微任務回調放在微隊列中,當所有的js代碼執行完畢,在退出全局上下文之前引擎會去檢查該隊列,有回調就執行,沒有就退出執行上下文,這也就是爲什麼微任務要早於宏任務,也是大家常說的,每個宏任務都有一個微任務隊列(由於定時器是瀏覽器的API,所以定時器是宏任務,在js中遇到定時器會也是放入到瀏覽器的隊列中)
- 例如
此時,你可能還很迷惑,沒關係,請接着往下看
圖解宏任務和微任務
首先執行一個宏任務,執行結束後判斷是否存在微任務
有微任務先執行所有的微任務,再渲染,沒有微任務則直接渲染
然後再接着執行下一個宏任務
圖解完整的Event Loop
首先,整體的script(作爲第一個宏任務)開始執行的時候,會把所有代碼分爲同步任務
、異步任務
兩部分
同步任務會直接進入主線程依次執行
異步任務會再分爲宏任務和微任務
宏任務進入到Event Table中,並在裏面註冊回調函數,每當指定的事件完成時,Event Table會將這個函數移到Event Queue中
微任務也會進入到另一個Event Table中,並在裏面註冊回調函數,每當指定的事件完成時,Event Table會將這個函數移到Event Queue中
當主線程內的任務執行完畢,主線程爲空時,會檢查微任務的Event Queue,如果有任務,就全部執行,如果沒有就執行下一個宏任務
上述過程會不斷重複,這就是Event Loop,比較完整的事件循環
關於Promise
new Promise(() => {}).then()
,我們來看這樣一個Promise代碼
前面的 new Promise()
這一部分是一個構造函數,這是一個同步任務
後面的 .then()
纔是一個異步微任務,這一點是非常重要的
new Promise((resolve) => {
console.log(1)
resolve()
}).then(()=>{
console.log(2)
})
console.log(3)
複製代碼
上面代碼輸出1 3 2
關於 async/await 函數
async/await本質上還是基於Promise的一些封裝,而Promise是屬於微任務的一種
所以在使用await關鍵字與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
以前的代碼,相當於與 new Promise
的同步代碼,await
以後的代碼相當於 Promise.then
的異步
舉慄印證
首先給大家來一個比較直觀的動圖
之所以放這個動圖,就是爲了向大家推薦這篇好文,動圖錄屏自參考鏈接【1】
極力推薦大家看看這篇帖子,非常nice,分步動畫生動且直觀,有時間的話可以自己去體驗下
不過在看這個帖子之前你要先了解下運行機制會更好讀懂些
接下來這個來自網上隨意找的一個比較簡單的面試題,求輸出結果
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)
複製代碼
結合我們上述的JS運行機制再來看這道題就簡單明瞭的多了
JS是順序從上而下執行
執行到test(),test方法爲同步,直接執行,console.log(1)
打印1
test方法中setTimeout爲異步宏任務,回調我們把它記做timer1放入宏任務隊列
接着執行,test方法下面有一個setTimeout爲異步宏任務,回調我們把它記做timer2放入宏任務隊列
接着執行promise,new Promise是同步任務,直接執行,打印4
new Promise裏面的setTimeout是異步宏任務,回調我們記做timer3放到宏任務隊列
Promise.then是微任務,放到微任務隊列
console.log(8)是同步任務,直接執行,打印8
主線程任務執行完畢,檢查微任務隊列中有Promise.then
開始執行微任務,發現有setTimeout是異步宏任務,記做timer4放到宏任務隊列
微任務隊列中的console.log(7)是同步任務,直接執行,打印7
微任務執行完畢,第一次循環結束
檢查宏任務隊列,裏面有timer1、timer2、timer3、timer4,四個定時器宏任務,按照定時器延遲時間得到可以執行的順序,即Event Queue:timer2、timer4、timer3、timer1,依次拿出放入執行棧末尾執行 (插播一條:瀏覽器 event loop 的 Macrotask queue,就是宏任務隊列在每次循環中只會讀取一個任務)
執行timer2,console.log(3)爲同步任務,直接執行,打印3
檢查沒有微任務,第二次Event Loop結束
執行timer4,console.log(6)爲同步任務,直接執行,打印6
檢查沒有微任務,第三次Event Loop結束
執行timer3,console.log(5)同步任務,直接執行,打印5
檢查沒有微任務,第四次Event Loop結束
執行timer1,console.log(2)同步任務,直接執行,打印2
檢查沒有微任務,也沒有宏任務,第五次Event Loop結束
結果:1,4,8,7,3,6,5,2
提一下NodeJS中的運行機制
上面的一切都是針對於瀏覽器的EventLoop
雖然NodeJS中的JavaScript運行環境也是V8,也是單線程,但是,還是有一些與瀏覽器中的表現是不一樣的
其實nodejs與瀏覽器的區別,就是nodejs的宏任務分好幾種類型,而這好幾種又有不同的任務隊列,而不同的任務隊列又有順序區別,而微任務是穿插在每一種宏任務之間的
在node環境下,process.nextTick的優先級高於Promise,可以簡單理解爲在宏任務結束後會先執行微任務隊列中的nextTickQueue部分,然後纔會執行微任務中的Promise部分
上圖來自NodeJS官網
如上圖所示,nodejs的宏任務分好幾種類型,我們只簡單介紹大體內容瞭解,不詳細解釋,不然又是囉哩囉嗦一大篇
NodeJS的Event Loop相對比較麻煩
Node會先執行所有類型爲 timers 的 MacroTask,然後執行所有的 MicroTask(NextTick例外)
進入 poll 階段,執行幾乎所有 MacroTask,然後執行所有的 MicroTask
再執行所有類型爲 check 的 MacroTask,然後執行所有的 MicroTask
再執行所有類型爲 close callbacks 的 MacroTask,然後執行所有的 MicroTask
至此,完成一個 Tick,回到 timers 階段
……
如此反覆,無窮無盡……
複製代碼
反觀瀏覽器中Event Loop就比較容易理解
先執行一個 MacroTask,然後執行所有的 MicroTask
再執行一個 MacroTask,然後執行所有的 MicroTask
……
如此反覆,無窮無盡……
複製代碼
好了,關於Node中各個類型階段的解析,這裏就不過多說明了,自己查閱資料吧,這裏就是簡單提一下,NodeJS的Event Loop解釋起來比瀏覽器這繁雜,這裏就只做個對比。
參考