JavaScript 運行機制--Event Loop詳解

JavaScript(簡稱JS)是前端的首要研究語言,要想真正理解JavaScript就繞不開他的運行機制--Event Loop(事件環)前端

JS是一門單線程的語言,異步操做是實際應用中的重要的一部分,關於異步操做參考個人另外一篇文章js異步發展歷史與Promise原理分析 這裏再也不贅述。node

堆、棧、隊列

堆(heap)

堆(heap)是指程序運行時申請的動態內存,在JS運行時用來存放對象。web

棧(stack)

棧(stack)遵循的原則是「先進後出」,JS種的基本數據類型與指向對象的地址存放在棧內存中,此外還有一塊棧內存用來執行JS主線程--執行棧(execution context stack),此文章中的棧只考慮執行棧。ajax

隊列(queue)

隊列(queue)遵循的原則是「先進先出」,JS中除了主線程以外還存在一個「任務隊列」(其實有兩個,後面再詳細說明)。vim

Event Loop

JS的單線程也就是說全部的任務都須要按照必定的規則順序排隊執行,這個規則就是咱們要說明的Event Loop事件環。Event Loop在不一樣的運行環境下有着不一樣的方式。promise

瀏覽器環境下的Event Loop

先上圖(轉自Philip Roberts的演講《Help, I'm stuck in an event-loop》) 瀏覽器

  • 當主線程運行的時候,JS會產生堆和棧(執行棧)
  • 主線程中調用的webaip所產生的異步操做(dom事件、ajax回調、定時器等)只要產生結果,就把這個回調塞進「任務隊列」中等待執行。
  • 當主線程中的同步任務執行完畢,系統就會依次讀取「任務隊列」中的任務,將任務放進執行棧中執行。
  • 執行任務時可能還會產生新的異步操做,會產生新的循環,整個過程是循環不斷的。

從事件環中不難看出當咱們調用setTimeout並設定一個肯定的時間,而這個任務的實際執行時間可能會因爲主線程中的任務沒有執行完而大於咱們設定的時間,致使定時器不許確,也是連續調用setTimeout與調用setInterval會產生不一樣效果的緣由(此處就再也不展開,有時間我會單獨寫一篇文章)。bash

接下來上代碼:dom

console.log(1);
console.log(2);
setTimeout(function(){
    console.log(3)
    setTimeout(function(){
        console.log(6);
    })
},0)
setTimeout(function(){
    console.log(4);
    setTimeout(function(){
        console.log(7);
    })
},0)
console.log(5)
複製代碼

代碼中的setTimeout的時間給得0,至關於4ms,也有可能大於4ms(不重要)。咱們要注意的是代碼輸出的順序。咱們把任務以其輸出的數字命名。 先執行的必定是同步代碼,先輸出1,2,5,而3任務,4任務這時會依次進入「任務隊列中」。同步代碼執行完畢,隊列中的3會進入執行棧執行,4到了隊列的最前端,3執行完後,內部的setTimeout將6的任務放入隊列尾部。開始執行4任務……異步

最終咱們獲得的輸出爲1,2,5,3,4,6,7。

宏任務與微任務

任務隊列中的全部任務都是會乖乖排隊的嗎?答案是否認的,任務也是有區別的,老是有任務會有一些特權(好比插隊),就是任務中的vip--微任務(micro-task),那些沒有特權的--宏任務(macro-task)。 咱們看一段代碼:

console.log(1);
setTimeout(function(){
    console.log(2);
    Promise.resolve(1).then(function(){
        console.log('promise')
    })
})
setTimeout(function(){
    console.log(3);
})
複製代碼

按照「隊列理論」,結果應該爲1,2,3,promise。但是實際結果事與願違輸出的是1,2,promise,3。

明明是3先進入的隊列 ,爲何promise會排在前面輸出?這是由於promise有特權是微任務,當主線程任務執行完畢微任務會排在宏任務前面先去執行,不論是不是後來的。

換句話說,就是任務隊列實際上有兩個,一個是宏任務隊列,一個是微任務隊列,當主線程執行完畢,若是微任務隊列中有微任務,則會先進入執行棧,當微任務隊列沒有任務時,纔會執行宏任務的隊列。

微任務包括: 原生Promise(有些實現的promise將then方法放到了宏任務中),Object.observe(已廢棄), MutationObserver, MessageChannel;

宏任務包括:setTimeout, setInterval, setImmediate, I/O;

Node環境下的Event Loop

┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘
複製代碼

node中的時間循環與瀏覽器的不太同樣,如圖:

  • timers 階段: 這個階段執行setTimeout(callback) and setInterval(callback)預約的callback;
  • I/O callbacks 階段: 執行除了close事件的callbacks、被timers(定時器,setTimeout、setInterval等)設定的callbacks、setImmediate()設定的callbacks以外的callbacks;
  • idle, prepare 階段: 僅node內部使用;
  • poll 階段: 獲取新的I/O事件, 適當的條件下node將阻塞在這裏;
  • check 階段: 執行setImmediate() 設定的callbacks;
  • close callbacks 階段: 好比socket.on(‘close’, callback)的callback會在這個階段執行。

每個階段都有一個裝有callbacks的fifo queue(隊列),當event loop運行到一個指定階段時, node將執行該階段的fifo queue(隊列),當隊列callback執行完或者執行callbacks數量超過該階段的上限時, event loop會轉入下一下階段。

process.nextTick

process.nextTick方法不在上面的事件環中,咱們能夠把它理解爲微任務,它的執行時機是當前"執行棧"的尾部----下一次Event Loop(主線程讀取"任務隊列")以前----觸發回調函數。也就是說,它指定的任務老是發生在全部異步任務以前。setImmediate方法則是在當前"任務隊列"的尾部添加事件,也就是說,它指定的任務老是在下一次Event Loop時執行。上代碼:

process.nextTick(function A() {
  console.log(1);
  process.nextTick(function B(){console.log(2);});
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED
複製代碼

代碼能夠看出,不只函數A比setTimeout指定的回調函數timeout先執行,並且函數B也比timeout先執行。這說明,若是有多個process.nextTick語句(無論它們是否嵌套),將所有在當前"執行棧"執行。

setTimeout 和 setImmediate

兩者很是類似,可是兩者區別取決於他們何時被調用.

  • setImmediate 設計在poll階段完成時執行,即check階段;
  • setTimeout 設計在poll階段爲空閒時,且設定時間到達後執行;但其在timer階段執行

其兩者的調用順序取決於當前event loop的上下文,若是他們在異步i/o callback以外調用,其執行前後順序是不肯定的。

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

setImmediate(function immediate () {
  console.log('immediate');
});
複製代碼
$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js
immediate
timeout
複製代碼

這是由於後一個事件進入的時候,事件環可能處於不一樣的階段致使結果的不肯定。當咱們給了事件環肯定的上下文,事件的前後就能肯定了。

var fs = require('fs')

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

這是由於由於fs.readFile callback執行完後,程序設定了timer 和 setImmediate,所以poll階段不會被阻塞進而進入check階段先執行setImmediate,後進入timer階段執行setTimeout。

相關文章
相關標籤/搜索