JS 事件機制 Event Loop

哇咔咔君發表於2019-04-09

談談Event Loop

無論是瀏覽器的Event loop 還是 Nodejs 中的Event loop,都是基於JS 的單執行緒設計而生的(純屬個人理解),通過事件迴圈實現非阻塞非同步執行效果。

幾個概念

談到事件機制首先弄清楚主執行緒執行棧任務佇列這幾個概念。

主執行緒:執行JS 程式碼。JS 的單執行緒針對的是單一的主執行緒,不是隻能有一個執行緒。

執行棧:關於執行棧的概念結合執行上下文來看,《JavaScript深入之執行上下文棧》

任務佇列Task Queue:即佇列,是一種先進先出的資料結構

三者的關係是:主執行緒要執行的都在執行棧裡,執行棧裡的內容是從任務佇列裡拿過來的。

關於任務

任務可以劃分為巨集任務微任務

  • 巨集任務MacroTask: 整個script , setTimeout, setInterval, setImmidiate(瀏覽器暫時不支援,只有IE10,但是在nodejs 中常用到)、I/O、UI Redering。

  • 微任務(MicroTask):Promise, Async/Await(底層就是Promise)、Process.nextTick (node獨有)、MutationObserver。

具體執行時,任務還有另一種的劃分就是同步任務非同步任務

對於同步任務來說 ,放入執行棧裡按序依次執行的,但是對於非同步任務而言,是在非同步任務有了結果之後將非同步任務的回撥函式放到任務佇列裡等待主執行緒空閒時執行。注意這裡是在執行棧中如果遇到非同步任務,是註冊非同步任務的回撥函式 ,把回撥函式放到任務佇列中(這一步會產出任務)。

瀏覽器中的Event Loop

執行流程

事件迴圈的程式模型

  • 執行棧一開始認為是空,將巨集任務: 整體的script 壓入棧執行。
  • 執行過程中遇到同步任務按序一步一步執行,遇到非同步任務註冊非同步任務的回撥函式放到對應的任務佇列中(這裡有巨集任務佇列,微任務佇列)。這樣就產生了新的macro-task 和 micro-task。
  • script 中程式碼執行完,將script 出棧,也是一次巨集任務出棧 。
  • 檢測任務佇列中是否有微任務,清理微任務佇列。這裡需要注意的是巨集任務出隊是一個一個出,而微任務是一隊一隊出。
  • 更新頁面渲染
  • 檢測是否有web worker 任務,處理web worker。
  • 上述過程迴圈往復,直到兩個佇列都清空。

總結一下整體的流程:

主執行緒首先會執行一個巨集任務,當此巨集任務執行完之後,會去檢視是否有微任務佇列。如果有,那就清空整個微任務佇列。如果沒有,會去檢視巨集任務佇列,取巨集任務佇列中的第一個去執行,執行巨集任務的過程中遇到微任務,依次加入微任務佇列等待下次執行。(當然執行微任務的時候也會產生巨集任務,主執行緒會放入巨集任務佇列)。棧空後,再次讀取任務佇列中的任務,以此類推。

舉個例子

 console.log('script start')
 setTimeout(() => {
     console.log('setTimeout 1');
     Promise.resolve().then(() => {
          console.log('promise1')
    })
}, 0)

 new Promise(resolve => {
    console.log('promise2');
    setTimeout(() => {
        console.log('setTimeout2');
        resolve();
    }, 0)
 })

 Promise.resolve().then(() => {
     console.log('promise3');
     setTimeout(() => {
          console.log('setTimeout 3')
    },0)
 }).then(() => console.log('promise4'))
 console.log('script end');
複製程式碼

輸出結果

script start 
promise2
script end
promise3
promise4
setTimeout1
promise1
setTimeout2
setTimeout3
複製程式碼

詳細執行流程如下:

1. 初始巨集任務佇列只有整體script,執行棧、微任務佇列皆為空

巨集任務script入棧執行:

輸出 script start

遇見setTimeout1放入巨集任務佇列

執行同步程式碼輸出 promise2,遇見setTimeout2放入巨集任務佇列

遇見微任務promise3promise4放入微任務佇列

輸出 script end

我們看下第一個巨集任務執行完的狀態:

巨集任務佇列: setTimeout1setTimeout2

微任務佇列: promise3promise4


2. 接下來去清理微任務佇列

微任務佇列遵循先進先出原則,promise3入棧執行:輸出 promise3、,將setTimeout3放入巨集任務佇列。promise3出棧 ,promise4入棧執行,輸出 promise4

清理完微任務後的狀態:

巨集任務佇列setTimeout1setTimeout2setTimeout3

微任務佇列:空


3. 從巨集任務佇列中取出第一個入棧執行

setTimeout1 入棧執行,輸出 setTimeout1 ,將 promise1放入微任務佇列

本次執行完的狀態:

巨集任務佇列setTimeout2setTimeout3

微任務佇列promise1


4. 清理微任務佇列

輸出 promise1

本輪執行完狀態:

巨集任務佇列setTimeout2setTimeout3

微任務佇列: 空


5、 從巨集任務佇列中取出第一個執行

輸出 setTimeout2

本輪執行完狀態:

巨集任務佇列setTimeout3

微任務佇列: 空


6. 微任務佇列為空,繼續從巨集任務佇列取出第一個執行

輸出 setTimeout3

至此巨集任務佇列微任務佇列都為空。

從上述執行過程中, 不難看出,瀏覽器的Event Loop 總是一個巨集任務 ——> 一隊微任務 這種順序依次迴圈

另一個例子

var a = async function () {
    await Promise.resolve().then(() => console.log(111));
    console.log(222)
}
a().then(() => console.log(333))

var b = async function () {
    await setTimeout(() => console.log('aaa'), 0);
    console.log('bbb')
}
b().then(() => console.log('ccc'))

var c = async function () {
    await console.log('A');
    console.log('B')
}
c().then(() => console.log('C'))

複製程式碼

輸出結果:

A -> 111 -> bbb -> B -> 222 -> ccc -> C -> 333 -> aaa

執行順序:

1. script入棧

執行同步程式碼,呼叫a():將111放入微任務佇列。繼續往下走執行b(),將aaa 放入巨集任務佇列,將 bbb 放入微任務佇列,接著執行c(), 列印 A, 將B 放入微任務佇列。

此次執行完的狀態:

巨集任務佇列: aaa

微任務佇列: 111bbbB

2. 清理微任務佇列

執行 111, 列印111 、將222放入微任務佇列,執行bbb:列印bbb並將 ccc放入微任務佇列,執行B: 列印B 並將C放入微任務佇列,此時微任務佇列並沒有清理完,所以接著列印222cccC,列印完222333放置微任務佇列,所以接著列印333

此次執行完的狀態:

巨集任務佇列: aaa

微任務佇列: 空

  1. 執行aaa, 列印出aaa

上述程式碼若是不好理解可以轉化為promise的方式來理解,效果如下:

new Promise(resolve => {
   	Promise.resolve()
        .then(() => console.log(111))
        .then(() => { 
	    console.log(222)
	    resolve()
	})
})
.then(() => console.log(333))

new Promise(resolve => {
	setTimeout(() => console.log('aaa'), 0)
	Promise.resolve().then(() => {
            console.log('bbb')
            resolve()
	})
	
})
.then(() => console.log('ccc'))

new Promise(resolve => {
	console.log('A')
	Promise.resolve()
	.then(() => {
	    console.log('B')
	    resolve()
	})
})
.then(() => console.log('C'))

複製程式碼

但這純屬個人理解,若有問題,還請大神指導。

Nodejs中的Event Loop

nodejs

1.簡介

Node 中的 Event Loop和瀏覽器中的是完全不相同的東西。Node.js採用V8作為js的解析引擎,而I/O處理方面使用了自己設計的libuvlibuv是一個跨平臺、專門寫給nodejs的庫,使用非同步,事件驅動的程式設計方式,核心是提供I/O的事件迴圈和非同步回撥。封裝了不同作業系統一些底層特性,對外提供統一的API。

2.六個階段

JS 事件機制 Event Loop

未完待續...

相關文章