js的事件循環機制

1、js 的定義和特性?

衆所周知,js 是一門單線程的非阻塞的腳本語言。javascript

單線程:只有一個調用棧,同一時刻只能幹一件事,代碼是一段一段執行的。java

調用棧:是一個數據結構,記錄咱們程序運行到哪個階段了,若是調用了函數就進棧,若是函數返回結果,就出棧(進棧出棧)。node

非阻塞:代碼須要進行一項異步任務的時候,主線程會掛起這個任務,而後在異步任務返回結果的時候,再根據一段的規則去執行相應的回調。web

爲何是單線程的?
這是由於 js 創立之初的目的就在於與瀏覽器交互,而瀏覽器要大量操做 dom,試想一下,若是同時對某個 dom 節點進行修改和刪除的操做,那會發生什麼呢?因此決定了 js 只能是單線程的。ajax

爲何非阻塞呢?
咱們在頁面中一般會發大量的請求,獲取後端的數據去渲染頁面。由於瀏覽器是單線程的,試想一下,當咱們發出異步請求的時候,阻塞了,後面的代碼都不執行了,那頁面可能出現長時間白屏,極度影響用戶體驗。後端

2、瀏覽器環境下?

這裏,咱們只談論 Google 的 js 引擎---V8 引擎(nodeJS 也是 v8 引擎)。api

1.瀏覽器環境下 js 是怎樣工做的?

1.1js 的引擎簡圖

主要是由兩部分組成:promise

  • 1.emory Heap(內存堆) —  內存分配地址的地方
  • 2.Call Stack(調用堆棧) — 代碼執行的地方
1.2.js 運行過程簡圖

  • 1.js 引擎(js 代碼執行在調用棧)
  • 2.webapis(瀏覽器提供給咱們的,不是 js 引擎提供的,例如:Dom,ajax,setTimeout)
  • 3.回調隊列(callback queue 包括宏任務,微任務)
  • 4.事件循環(event loop)
1.3.js 運行過程:
  • 1.當 js 運行時,碰到同步任務,就在stack裏執行
  • 2.一旦碰到異步任務,主線程會掛起這個任務,把異步回調結果放在callback queue裏。
  • 3.等待當前stack中的全部任務都執行完畢,主線程處於閒置狀態時,主線程會去查找callback queue是否有任務。若是有,那麼主線程會從中取出回調(此處區分宏任務與微任務)放入stack中,而後執行其中的同步代碼...,如此反覆。
    因爲第三步,這樣就造成了一個無限的循環。這就是這個過程被稱爲「事件循環(Event Loop)」的緣由

2.瀏覽器環境下 js 的事件循環機制?

2.1 宏任務和微任務(callback queue回調隊列裏面2條平行的隊列,宏任務隊列和微任務隊列,宏任務隊列裏面放宏任務的回調,微任務隊列裏面放微任務的回調)
  • 宏任務:script(總體代碼),setInterval(),setTimeout(),setImmediate(Nodejs), I/O, UI rendering
  • 微任務:process.nextTick(Nodejs),Promises,Object.observe, MutationObserver
2.2 js 事件循環代碼
console.log(1);
setTimeout(function a() {
  console.log(2);
}, 0);
new Promise(function (resolve, reject) {
  console.log(5);
  resolve();
}).then(function () {
  console.log(6);
});
new Promise(function (resolve, reject) {
  resolve();
}).then(function () {
  console.log(7);
});
console.log(3);

結果:1,5,3,6,2瀏覽器

分析:代碼從上往下執行,先打印同步任務,1。碰到 setTimeout,把回調函數 a()放到 callback queue 的宏任務裏去。而後碰到 Promise,打印 new Promise 的同步任務 5,接着把 then 回調(console.log(6)),放入 callback queue 的微任務裏去,而後打印同步任務 3。此時 call stack 爲空,去查找 callback queue,微任務比宏任務先,且當前循環會處理當前全部微任務隊列中的事件。因此,先打印 6,再打印 7,在打印 2.
總結:先執行同步任務,再執行微任務,最後執行宏任務
2.3.屢次 js 事件循環
let promiseGlobal = new Promise(function (resolve) {
  console.log(1);
  resolve("2");
});
console.log(3);

promiseGlobal.then(function (data) {
  console.log(data);
  let setTimeoutInner = setTimeout(function (_) {
    console.log(4);
  }, 1000);
  let promiseInner = new Promise(function (resolve) {
    console.log(5);
    resolve(6);
  }).then(function (data) {
    console.log(data);
  });
});
let setTimeoutGlobal = setTimeout(function (_) {
  console.log(7);
  let promiseInGlobalTimeout = new Promise(function (resolve) {
    console.log(8);
    resolve(9);
  }).then(function (data) {
    console.log(data);
  });
}, 1000);

執行順序是 1,3,2,5,6,間隔一秒,7,8,9,4網絡

解答以下:

  • 1.打印完 1,3
    本輪執行棧執行完畢
  • 2.打印完 1,3,2,5,6
    微任務隊列清空,eventloop 完成,下一次 eventloop 開始
  • 3.打印完 1,3,2,5,6,7
    本輪執行棧執行完畢
  • 4.打印完 1,3,2,5,6,7,8,9
    微任務隊列清空,eventloop 完成,下一次 eventloop 開始
  • 5.打印完 1,3,2,5,6,7,8,9,4
    eventloop 完成
⚠️易錯點:
 之因此把這道題拿出來說,是由於這道題涉及到屢次事件循環,不少同窗容易搞混的點。

3.總結

  • 當前執行棧執行完畢,會當即處理全部微任務隊列中的事件,再去宏任務隊列中取出一個事件
  • 在一次事件循環中,微任務永遠在宏任務以前執行

2、宏任務、微任務、Dom 渲染的順序

1.瀏覽器包含多個進程

  • 1.主進程

    • 協調控制其餘子進程(建立、銷燬)
  • 2.第三方插件進程

    • 每種類型的插件對應一個進程,僅當使用該插件時才建立
  • 3.GPU 進程

    • 用於 3D 繪製等
  • 4.渲染進程,就是咱們說的瀏覽器內核(最重要

    • 負責頁面渲染,腳本執行,事件處理等
    • 每一個 tab 頁一個渲染進程

2.渲染進程包含了多個線程:

  • 1.JS 引擎線程

    • 負責處理解析和執行 javascript 腳本程序
    • 只有一個 JS 引擎線程(單線程)
    • 與 GUI 渲染線程互斥,防止渲染結果不可預期
  • 2.GUI 渲染線程

    • 負責渲染頁面,佈局和繪製
    • 頁面須要重繪和迴流時,該線程就會執行
    • 與 js 引擎線程互斥,防止渲染結果不可預期
  • 3.http 請求線程

    • 瀏覽器有一個單獨的線程用於處理 AJAX 請求
  • 4.事件處理線程(鼠標點擊、ajax 等)

    • 用來控制事件循環(鼠標點擊、setTimeout、ajax 等)
  • 5.定時器觸發線程

    • setInterval 與 setTimeout 所在的線程

3. 爲何 JS 引擎線程和 GUI 渲染線程是互斥的?

JavaScript 是可操縱 DOM 的,若是在修改這些元素屬性同時渲染界面,那麼渲染線程先後得到的元素數據就可能不一致了。所以爲了防止渲染出現不可預期的結果,瀏覽器設置 GUI 渲染線程與 JS 引擎爲互斥的關係,當 JS 引擎執行時 GUI 線程會被掛起,GUI 更新則會被保存在一個隊列中等到 JS 引擎線程空閒時當即被執行。

4. 爲何 JS 會阻塞頁面加載?

從上面的互斥關係能夠推導出,JS 若是執行時間過長就會阻塞頁面。譬如,假設 JS 引擎正在進行巨量的計算,此時就算 GUI 有更新,也會被保存到隊列中,等待 JS 引擎空閒後執行。而後,因爲巨量計算,因此 JS 引擎極可能好久好久後才能空閒,天然會感受到巨卡無比。因此,要儘可能避免 JS 執行時間過長,這樣就會形成頁面的渲染不連貫,致使頁面渲染加載阻塞的感受。

5. JS 引擎線程和 GUI 渲染線程是互斥的,那前後順序呢?

把下面三段代碼放到瀏覽器的控制檯執行:
document.body.style = "background:black";
document.body.style = "background:red";
document.body.style = "background:blue";
document.body.style = "background:grey";

結果:背景直接變成灰色
分析:Call Stack 清空的時候,執行,執行到了 document.body.style = 'background:grey';這時,前面的代碼都被覆蓋了,此時 dom 渲染,背景色是灰色

document.body.style = "background:blue";
console.log(1);
Promise.resolve().then(function () {
  console.log(2);
  document.body.style = "background:black";
});
console.log(3);

結果:背景直接變成黑色
分析:document.body.style = 'background:blue'是同步代碼,document.body.style = 'background:black'是微任務,此時微任務執行完,纔會進行 dom 渲染,因此背景色是黑色

document.body.style = "background:blue";
setTimeout(function () {
  document.body.style = "background:black";
}, 0);

結果:背景先一閃而過藍色,而後變成黑色
分析:document.body.style = 'background:blue';是同步代碼,document.body.style = 'background:black'是宏任務,因此 dom 在同步代碼執行完,宏任務執行以前會渲染一次。而後宏任務執行完又會渲染一次。2 次渲染,因此纔會呈現背景先一閃而過藍色,而後變成黑色,這種效果。

總結:
1.先把Call Stack清空
2.而後執行當前的微任務
3.接下來DOM渲染
微任務在dom渲染`以前`執行,宏任務在dom渲染`以後`執行。

3、nodeJs 環境下的 js 事件循環機制

⚠️ 注意:如下內容 node 的版本大於等於 11.0.0

1.NodeJs 的架構圖


解釋:

  • 1.Node Standard Library:Node.js 標準庫,這部分是由 Javascript 編寫的。使用過程當中直接能調用的 API。例如模塊 http、buffer、fs、stream 等
  • 2.Node bindings:這裏就是 JavaScript 與 C/C++ 鏈接的橋樑,前者經過 bindings 調用後者,相互交換數據。
  • 3.最下面一層是支撐 Node.js 運行的關鍵,由 C/C++ 實現(好比:V8:Google 開源的高性能 JavaScript 引擎,使用 C++ 開發)

2.libuv 引擎

libuv 專一於異步 I/O.是一個基於事件驅動的跨平臺抽象層,封裝了不一樣操做系統的一些底層特性,對外提供統一 API,Node.js的Event Loop 是基於libuv實現的

3.node 環境下 js 是怎樣運行的?

  • (1)V8 引擎解析 JavaScript 腳本。
  • (2)解析後的代碼,調用 Node API。
  • (3)libuv 庫負責 Node API 的執行。它將不一樣的任務分配給不一樣的線程,造成一個 Event Loop(事件循環),以異步的方式將任務的執行結果返回給 V8 引擎。
  • (4)V8 引擎再將結果返回給用戶

4.js 事件循環的階段

  • 1.timer:這個階段執行 timer(setTimeout、setInterval)的回調
  • 2.I/O callbacks 階段:執行一些系統調用錯誤,好比網絡通訊的錯誤回調(tcp 錯誤)
  • 3.idle,prepare 階段:僅供 node 內部使用,忽略
  • 4.poll 階段:檢索新的 I/O 事件;執行與 I/O 相關的回調(幾乎全部狀況下,除了關閉的回調函數,那些由計時器和 setImmediate() 調度的以外),其他狀況 node 將在適當的時候在此阻塞。
  • 5.check:setImmediate() 回調函數在這裏執行
  • 6.close callbacks:一些關閉的回調函數,如:socket.on('close', ...)

5.process.nextTick 和 microtask 的特別

都是獨立於 Event Loop 以外的,它有一個本身的隊列,當每一個階段完成後,若是存在 nextTick 隊列,就會清空隊列中的全部回調函數,而且優先於其餘 microtask 執行。

6.js 事件循環的順序

  • 宏任務:script(總體代碼),setInterval(),setTimeout(),setImmediate(Nodejs), I/O, UI rendering
  • 微任務:process.nextTick(Nodejs),Promises,Object.observe, MutationObserver
setTimeout(funciton(){console.log(1)});
setImmediate(function(){console.log(2)});
process.nextTick(function(){console.log(3)});
Promise.resolve().then(function(){console.log(4)});
(function() {console.log(5)})();

打印結果:5,3,4,1,2

總結:先執行同步任務,接下來執行 process.nextTick,再接下來 Promise 的微任務,最後是 js 事件循環的 6 個階段,從上到下順序執行。

⚠️ 注意:每一個階段都有一個先進先出的回調函數隊列。只有一個階段的回調函數隊列清空了,該執行的回調函數都執行了,事件循環纔會進入下一個階段。

4、瀏覽器和nodeJs 環境下的 js 事件循環機制對比

1. 瀏覽器下打印:time1,promise1,time2,promise2

由於執行完2個定時器,回調都進入宏任務隊列了。而後開始事件循環,由於宏任務是一個個執行的,因此先把第一個定時器的回調放入調用棧中,執行完time1,把微任務放入微任務隊列中。
這是調用棧清空,又開始事件循環,這時候有微任務promise1,和第二個宏任務。由於微任務在宏任務以前執行,因此先執行promise1,
這是調用棧又清空,又開始事件循環。執行第二個宏任務,打印,time2,promise2

2.node環境下打印:time1,timer2,promise1,promise2

由於已經在timer階段了,因此。先執行完time階段,time1,time2,而後看到微任務,執行微任務。

參考文章

1.從多線程到 Event Loop 全面梳理

相關文章
相關標籤/搜索