詳解JS運行機制和Event Loop

1 JS運行機制詳解

1.1 單線程的JS

javascript是一門單線程語言,在最新的HTML5中提出了Web-Worker,但javascript是單線程這一核心仍未改變。因此一切javascript版的"多線程"都是用單線程模擬出來的,一切javascript多線程都是紙老虎!javascript

1.2 Event Loop

既然js是單線程,後一個任務會等前一個任務執行完成後纔會執行,若是前一個任務執行時間過長後面的任務一直得不到執行,就會引發阻塞。那麼問題來了,假如咱們想瀏覽新聞,可是新聞包含的超清圖片加載很慢,難道咱們的網頁要一直卡着直到圖片徹底顯示出來?所以咱們會將任務分爲兩類:html

  • 同步任務
  • 異步任務

當咱們打開網站時,網頁的渲染過程就是一大堆同步任務,好比頁面骨架和頁面元素的渲染。而像加載圖片音樂之類佔用資源大耗時久的任務,就是異步任務。具體邏輯見下面的導圖: java

js執行機制

文字描述node

  • 同步和異步任務分別進入不一樣的執行"場所",同步的進入主線程,異步的進入Event Table並註冊函數。
  • 當指定的事情完成時,Event Table會將這個函數移入Event Queue。
  • 主線程內的任務執行完畢爲空,會去Event Queue讀取對應的函數,進入主線程執行。
  • 上述過程會不斷重複,也就是常說的Event Loop(事件循環)。準確的講,event loop是實現異步的一種機制。

上圖中Event Queue 包括 macro task queue 和 micro task queue,下一小節咱們會詳細解釋一下。 上代碼咱們體會一下這個流程:網絡

console.log('1');
setTimeout(function () {
    console.log('timeout');
});
console.log('2');
複製代碼

上面的代碼解釋多線程

  • console.log('1');console.log('2');是同步任務會放到主線程中,setTimeout聲明的回調函數會放到Event Table。主線程內的任務(console.log('1');console.log('2');)執行完畢爲空,會去Event Queue讀取console.log('timeout');,進入主線程執行。因此執行的結果爲1 2 timeout

1.3 Evnet Loop 中的macro task 和 micro task

1.3.1 定義

  • macro-task(宏任務):包括總體代碼script,setTimeout,setInterval, setImmediate(node環境下)。
  • micro-task(微任務):Promise,process.nextTick

下面兩張圖爲Event Loop 和 macro-task 及 micro-task的關係異步

event loop & macro task & micro task1

event loop & macro task & micro task2

導圖解釋ide

  • 不一樣類型的任務會進入對應的Event Queue,好比setTimeout和setInterval會進入macro task Queue, Promise 會進入 micro task Queue。
  • 事件循環的順序,決定js代碼的執行順序。
  • 進入總體代碼(宏任務)後,開始第一次循環。接着執行全部的微任務。而後再次從宏任務開始,找到其中一個任務隊列執行完畢,再執行全部的微任務。

1.3.2 e.g.

看到這麼多的定義和導圖,咱們來段代碼屢一下:函數

console.log('1');

setTimeout(function () {
    console.log('2');
    process.nextTick(function () {
        console.log('3');
    });
    new Promise(function (resolve) {
        console.log('4');
        resolve();
    }).then(function () {
        console.log('5')
    })
})
process.nextTick(function () {
    console.log('6');
})
new Promise(function (resolve) {
    console.log('7');
    resolve();
}).then(function () {
    console.log('8')
})
複製代碼
  • 總體script做爲第一個宏任務進入主線程,遇到console.log,輸出1。
  • 遇到setTimeout,其回調函數被分發到 macro task Queue中。
  • 遇到process.nextTick(),其回調函數被分發到micro task Queue中。咱們記爲process1。
  • 遇到Promise,new Promise直接執行,輸出7。then被分發到micro task Queue中。咱們記爲then1。
macro task Queue macro task Queue
setTimeout process1
- then1
  • 咱們發現了process1和then1兩個微任務。
  • 執行process1,輸出6。
  • 執行then1,輸出8。
  • 好了,第一輪事件循環正式結束,這一輪的結果是輸出1,7,6,8。那麼第二輪時間循環從setTimeout宏任務開始:
  • 遇到console.log,輸出2。
  • 遇到process.nextTick(),一樣將其分發到micro task Queue中,記爲process2。new Promise當即執行輸出4,then也分發到macro task Queue中,記爲then2。
macro task Queue macro task Queue
- process2
- then2
  • 咱們發現了process2和then2兩個微任務。
  • 執行process2,輸出3。
  • 執行then2,輸出5。
  • 好了,第一輪事件循環正式結束,這一輪的結果是輸出2,4,3,5。循環結束。最終的結果爲1 7 6 8 2 4 3 5

1.4 總結

  • javascript是一門單線程語言
  • 事件循環是js實現異步的一種方法,也是js的執行機制。

2 Node中的Event Loop

2.1 node中Event Loop執行順序

2.1.1 node中Event Loop的執行順序的簡單介紹

下圖爲node中Event Loop的執行順序的簡略圖 oop

node中Event Loop執行順序的簡略圖

note

  • timers: 執行被setTimeout() 和 setInterval()註冊的回調函數.
  • I/O callbacks: 執行除了 close事件的回調、 被 timers和setImmediate()註冊的回調.
  • idle, prepare: node內部執行
  • poll: 輪詢獲取新的 I/O 事件; node有可能會在這個地方阻塞.
  • check: 在這裏調用setImmediate() 註冊的回調.
  • close: 執行close事件的回調

2.1.2 詳解poll階段

1.poll階段的功能

  • 執行剛剛過時的計時器的腳本。
  • 在輪詢隊列中處理事件。

2.poll階段的處理流程

下面我用if else的方式描述一下poll階段的處理邏輯,以下:

if ('事件循環進入到 poll 階段 ' && '沒有timers註冊的scripts') {
    if ('poll 隊列 不爲空') {
        console.log('循環遍歷它的回調隊列,以同步執行它們,直到隊列耗盡,或者達到系統依賴的最大值');
    } else {
        if ('存在setImmediate()註冊的scripts') {
            console.log('結束poll phase 進入到check phase 執行這些註冊的scripts');
        } else {
            console.log('事件循環將等待被添加到隊列中的回調,而後當即執行它們');
        }
    }
}
console.log('一旦輪詢隊列爲空,事件循環將檢查有無到期的計時器。若是有一個或多個計時器準備就緒,事件循環將返回到計時器階段,以執行這些計時器的回調。');
複製代碼

3.比較setImmediate() 和 setTimeout()

setImmediate()setTimeout()很類似的,它們什麼時候被調用,決定了它們的行爲方式的不一樣。

  • setImmediate 用於在當前輪詢階段完成後執行腳本
  • setTimeout用於把註冊的腳本在最小閾值結束後運行。

它們執行的順序將根據調用它們的上下文而變化。若是兩個都是從主模塊中調用,那麼它們將受到進程性能的約束(這可能會受到其餘應用程序的影響)。

例如,若是咱們運行的腳本不是在I/O循環中(即主模塊),那麼執行兩個定時器的順序是不肯定的,由於它受過程性能的約束:

setTimeout(() => {
  console.log('timeout');
}, 0);

setImmediate(() => {
  console.log('immediate');
});
// 打印結果的前後順序是不肯定的,有時`timeout`在前,有時'immediate'在前
複製代碼

可是,若是把這段代碼放到I/O循環的回調中,immediate老是先被打印出來,以下:

const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});
// 在一個I/O週期內,在任何計時器的狀況下,setImmediate的回調,由於在一個I/O週期內,I/O callback 的下一個階段爲setImmediate的回調。
複製代碼

2.2 node的Event Loop實現

以下圖:

node中的EventLoop

說明

    1. Node的Event Loop分階段,階段有前後,依次是:
    • expired timers and intervals,即到期的setTimeout/setInterval
    • I/O events,包含文件,網絡等等
    • immediates,經過setImmediate註冊的函數
    • close handlers,close事件的回調,好比TCP鏈接斷開
    1. 同步任務及每一個階段以後都會清空microtask隊列
    • 優先清空next tick queue,即經過process.nextTick註冊的函數
    • 再清空other queue,常見的如Promise
    1. node會清空當前所處階段的隊列,即執行全部task

咱們在回頭看一下,下面的代碼:

setTimeout(() => {
  console.log('timeout');
}, 0);

setImmediate(() => {
  console.log('immediate');
});
複製代碼

能夠看出因爲兩個setTimeout延時相同,被合併入了同一個expired timers queue,而一塊兒執行了。因此,只要將第二個setTimeout的延時改爲超過2ms(1ms無效,由於最小間隔爲1s),就能夠保證這兩個setTimeout不會同時過時,也可以保證輸出結果的一致性。

咱們在回頭看一下,上面提到的另一段代碼:

const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});
複製代碼

爲什麼這樣的代碼能保證setImmediate的回調優先於setTimeout的回調執行呢?由於當兩個回調同時註冊成功後,當前node的Event Loop正處於I/O queue階段,而下一個階段是immediates queue,因此可以保證即便setTimeout已經到期,也會在setImmediate的回調以後執行。

3 補充

因爲水平有限,理解的程度可能會有誤差,歡迎你們指正。

4 參考文章

相關文章
相關標籤/搜索