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

前言

從開始作前端到目前爲止,陸續看了不少帖子講 JS 運行機制,看過不久就忘了,仍是本身理一遍好些css

經過碼字使本身對 JS 運行機制相關內容更加深入(本身用心寫過的貼子,內容也會牢記於心)html

順道給你們看看(我太難了,深夜碼字,反覆修改,說這麼多就是想請你點個贊在看)前端

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

是時候搞一波我大 js 了node

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

本文大體分爲如下這樣的步驟來幫助咱們由廣入深更加清晰的瞭解 JS 運行機制面試

  • 首先咱們要了解進程和線程的概念
  • 其次咱們要知道瀏覽器的進程線程常識
  • 再而後經過 Event Loop、宏任務(macrotask)微任務(microtask)來看瀏覽器的幾個線程間是怎樣配合的
  • 再而後經過例子來印證咱們的猜測
  • 最後提下 NodeJS 的運行機制

原創,首發於掘金ajax

靈魂一問

JS 運行機制在日常前端面試時無論是筆試題仍是面試題命中率都極高算法

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

看到這你們可能會說: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 引擎處理

定時觸發器線程

  • setIntervalsetTimeout所在線程
  • 瀏覽器定時計數器並非由 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')1

至此主線程執行棧中執行完畢,JS引擎線程已經空閒,開始向事件觸發線程發起詢問,詢問事件觸發線程的事件隊列中是否有須要執行的回調函數,若是有將事件隊列中的回調事件加入執行棧中,開始執行回調,若是事件隊列中沒有回調,JS引擎線程會一直髮起詢問,直到有爲止

到了這裏咱們發現,瀏覽器上的全部線程的工做都很單一且獨立,很是符合單一原則

定時觸發線程只管理定時器且只關注定時不關心結果,定時結束就把回調扔給事件觸發線程

異步 http 請求線程只管理 http 請求一樣不關心結果,請求結束把回調扔給事件觸發線程

事件觸發線程只關心異步回調入事件隊列

而咱們 JS 引擎線程只會執行執行棧中的事件,執行棧中的代碼執行完畢,就會讀取事件隊列中的事件並添加到執行棧中繼續執行,這樣反反覆覆就是咱們所謂的事件循環(Event Loop)

圖解

Xnip2020-01-14_09-51-28

首先,執行棧開始順序執行

判斷是否爲同步,異步則進入異步線程,最終事件回調給事件觸發線程的任務隊列等待執行,同步繼續執行

執行棧空,詢問任務隊列中是否有事件回調

任務隊列中有事件回調則把回調加入執行棧末尾繼續從第一步開始執行

任務隊列中沒有事件回調則不停發起詢問

宏任務(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"

Jan-12-2020 01-05-49

咱們看到上面動圖背景直接渲染了粉紅色,根據上文裏講瀏覽器會先執行完一個宏任務,再執行當前執行棧的全部微任務,而後移交 GUI 渲染,上面四行代碼均屬於同一次宏任務,所有執行完纔會執行渲染,渲染時GUI線程會將全部 UI 改動優化合並,因此視覺上,只會看到頁面變成粉紅色

再接着看

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

Jan-12-2020 01-28-41

上述代碼中,頁面會先卡一下藍色,再變成黑色背景,頁面上寫的是 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)

Jan-12-2020 01-31-42

控制檯輸出 1 3 2 , 是由於 promise 對象的 then 方法的回調函數是異步執行,因此 2 最後輸出

頁面的背景色直接變成粉色,沒有通過藍色的階段,是由於,咱們在宏任務中將背景設置爲藍色,但在進行渲染前執行了微任務, 在微任務中將背景變成了粉色,而後才執行的渲染

微任務宏任務注意點

  • 瀏覽器會先執行一個宏任務,緊接着執行當前執行棧產生的微任務,再進行渲染,而後再執行下一個宏任務

    微任務和宏任務不在一個任務隊列,不在一個任務隊列

    • 例如setTimeout是一個宏任務,它的事件回調在宏任務隊列,Promise.then()是一個微任務,它的事件回調在微任務隊列,兩者並非一個任務隊列
    • 以 Chrome 爲例,有關渲染的都是在渲染進程中執行,渲染進程中的任務(DOM 樹構建,js 解析…等等)須要主線程執行的任務都會在主線程中執行,而瀏覽器維護了一套事件循環機制,主線程上的任務都會放到消息隊列中執行,主線程會循環消息隊列,並從頭部取出任務進行執行,若是執行過程當中產生其餘任務須要主線程執行的,渲染進程中的其餘線程會把該任務塞入到消息隊列的尾部,消息隊列中的任務都是宏任務
    • 微任務是如何產生的呢?當執行到 script 腳本的時候,js 引擎會爲全局建立一個執行上下文,在該執行上下文中維護了一個微任務隊列,當遇到微任務,就會把微任務回調放在微隊列中,當全部的 js 代碼執行完畢,在退出全局上下文以前引擎會去檢查該隊列,有回調就執行,沒有就退出執行上下文,這也就是爲何微任務要早於宏任務,也是你們常說的,每一個宏任務都有一個微任務隊列(因爲定時器是瀏覽器的 API,因此定時器是宏任務,在 js 中遇到定時器會也是放入到瀏覽器的隊列中)

此時,你可能還很迷惑,不要緊,請接着往下看

圖解宏任務和微任務

Xnip2020-01-14_00-58-17

首先執行一個宏任務,執行結束後判斷是否存在微任務

有微任務先執行全部的微任務,再渲染,沒有微任務則直接渲染

而後再接着執行下一個宏任務

圖解完整的 Event Loop

Xnip2020-01-14_17-20-44

首先,總體的 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的異步

舉慄印證

首先給你們來一個比較直觀的動圖

Jan-14-2020 00-03-22

之因此放這個動圖,就是爲了向你們推薦這篇好文,動圖錄屏自參考連接【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

微任務執行完畢,第一次循環結束

檢查宏任務隊列,裏面有 timer一、timer二、timer三、timer4,四個定時器宏任務,按照定時器延遲時間獲得能夠執行的順序,即 Event Queue:timer二、timer四、timer三、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 部分

Xnip2020-01-18_14-50-01

上圖來自 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 解釋起來比瀏覽器這繁雜,這裏就只作個對比

最後

上面的流程圖都是本身畫的,因此有點 low,見諒

水平有限,歡迎指錯

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

最近拾起了一個被凍結的公衆號,又從新搞了下

歡迎你們關注【不正經的前端】,加我,加羣,或者拿一些資料均可以的,時不時發一些優質原創

參考

  1. Tasks, microtasks, queues and schedules - 重點推薦閱讀
  2. 聊聊 JavaScript 與瀏覽器的那些事 - 引擎與線程
  3. 前端文摘:深刻解析瀏覽器的幕後工做原理
  4. 瀏覽器進程?線程?傻傻分不清楚!
  5. 從輸入 cnblogs.com 到博客園首頁徹底展現發生了什麼
  6. 前端必讀:瀏覽器內部工做原理
  7. 什麼是 Event Loop?
  8. JavaScript 運行機制詳解:再談 Event Loop
  9. 單線程與多線程的區別
  10. 瀏覽器進程/線程模型及 JS 運行機制
  11. 瀏覽器的運行機制—2.瀏覽器都包含哪些進程?
  12. JS 必定要放在 Body 的最底部麼?聊聊瀏覽器的渲染機制
  13. 從瀏覽器多進程到 JS 單線程,JS 運行機制最全面的一次梳理
  14. 「前端進階」從多線程到 Event Loop 全面梳理
  15. Js 基礎知識(四) - js 運行原理與機制
  16. 這一次,完全弄懂 JavaScript 執行機制
  17. 前端性能優化:細說瀏覽器渲染的重排與重繪
  18. 10 分鐘看懂瀏覽器的渲染過程及優化
相關文章
相關標籤/搜索