你真的瞭解Event Loop(事件環)嗎?

JS是單線程的

JavaScript語言最大特色就是單線程,可是這裏的單線程指的是主線程是單線程的。那爲何js要單線程呢? 由於,JS主要用於操做DOM,若是是有兩個線程,一個在DOM上添加內容,一個在DOM上刪除內容,此時瀏覽器該以哪一個爲準呢? 因此爲了不復雜性,JavaScript從誕生起就是單線程的。html

同步和異步

同步和異步關注的是消息通知機制前端

  • 1)同步在發出調用後,沒有結果前是不返回的,一旦調用返回,就獲得返回值。調用者會主動等待這個調用結果。
  • 2)異步是發出調用後,調用者不會馬上獲得結果,而是被調用者經過狀態或回調函數來處理這個調用。

任務隊列

  • 由於JavaScript是單線程的。就意味着全部任務都須要排隊,前一個任務結束,後一個任務才能執行。前一個任務耗時很長,後一個任務也得一直等着。可是IO設備(好比ajax網絡請求)很慢,CPU一直初一顯得狀態,這樣就很不合理了。
  • 因此,其實主線程徹底能夠無論IO設備,掛起處於等待中的任務,先運行排在後面的任務。等到IO設備返回告終果,再回過頭,把掛起的任務繼續執行下去。因而有了同步任務異步任務

同步任務是指在主線程上執行的任務,只有前一個任務執行完畢,下一個任務才能執行。 異步任務是指不進入主線程,而是進入任務隊列(task queue)的任務,只有主線程任務執行完畢,任務隊列的任務纔會進入主線程執行。node

瀏覽器中Event Loop

event loop

從上圖看到:git

  1. 主線程運行的時候產生堆(heap)和棧(stack)
  2. 棧中的代碼調用各類外部API,它們在"任務隊列"中加入各類事件(click,load,done)
  3. 只要棧中的代碼執行完畢,主線程就會去讀取"任務隊列",將隊列中的事件放到執行棧中依次執行。
  4. 主線程繼續執行,當再調用外部API時又加入到任務隊列中,等主線程執行完畢又會接着將任務隊列中的事件放到主線程中。
  5. 上面整個過程是循環不斷的。

Node 的 Event Loop

Node.js也是單線程的Event Loop,可是它的運行機制不一樣於瀏覽器環境。github

node的event loop

根據上圖,Node.js的運行機制以下:ajax

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

除了setTimeoutsetInterval這兩個方法,Node.js還提供了另外兩個與"任務隊列"有關的方法:process.nextTicksetImmediateapi

process.nextTick方法能夠在當前"執行棧"的尾部----下一次Event Loop(主線程讀取"任務隊列")以前----觸發回調函數。也就是說,它指定的任務老是發生在全部異步任務以前。setImmediate方法則是在當前"任務隊列"的尾部添加事件,也就是說,它指定的任務老是在下一次Event Loop時執行,這與setTimeout(fn, 0)很像。promise

英文原文: When Node.js starts, it initializes the event loop, processes the provided input script (or drops into the REPL, which is not covered in this document) which may make async API calls, schedule timers, or call process.nextTick(), then begins processing the event loop.瀏覽器

當Node.js啓動時會初始化event loop, 每個event loop都會包含按以下順序六個循環階段bash

┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘
  • 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的任何階段執行,而是在各個階段切換的中間執行,即從一個階段切換到下個階段前執行。

宏任務和微任務

任務可分爲宏任務和微任務

常見的宏任務和微任務:

  1. macro-task(宏任務): setTimeout, setInterval, setImmediate, I/O
  2. micro-task(微任務):process.nextTick, 原生Promise(有些實現的promisethen方法放到了宏任務中),Object.observe(已廢棄), MutationObserver

看下面的例子:

console.log('script start');

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

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

上面代碼的執行順序是什麼呢?這是爲何呢?

script start
script end
promise1
promise2
setTimeout

Task 是嚴格按照時間順序壓棧和執行的,因此瀏覽器可以使得 JavaScript 內部任務與 DOM 任務可以有序的執行。當一個 task 執行結束後,在下一個 task 執行開始前,瀏覽器能夠對頁面進行從新渲染。每個 task 都是須要分配的,例如從用戶的點擊操做到一個點擊事件,渲染HTML文檔,同時還有上面例子中的 setTimeout

基於前面描述的event loopsetTimeout 它會在延遲時間結束後分配一個新的 taskevent loop 中,而不是當即執行,因此 setTimeout 的回調函數會等待前面的 task 都執行結束後再運行。這就是爲何 setTimeout 會輸出在 script end 以後,由於 script end 是第一個 task 的其中一部分,而 setTimeout 則是一個新的 task

微任務一般來講就是須要在當前 task 執行結束後當即執行的任務,例如須要對一系列的任務作出迴應,或者是須要異步的執行任務而又不須要分配一個新的 task,這樣即可以減少一點性能的開銷。

微任務任務隊列是一個與 task 任務隊列相互獨立的隊列,微任務將會在每個 task 任務執行結束以後執行。每個 task 中產生的 微任務 都將會添加到 微任務 隊列中,微任務 中產生的 微任務 將會添加至當前隊列的尾部,而且 微任務 會按序的處理完隊列中的全部任務。

  每當一個 Promise 被決議(或是被拒絕),便會將其回調函數添加至 微任務隊列中做爲一個新的 微任務。這也保證了 Promise 能夠異步的執行。因此當咱們調用 .then(resolve, reject)的時候,會當即生成一個新的 微任務添加至隊列中,這就是爲何上面的 promise1promise2 會輸出在 script end 以後,由於 微任務隊列中的任務必須等待當前 task 執行結束後再執行,而 promise1promise2 輸出在 setTimeout 以前,這是由於 setTimeout 是一個新的 task,而 微任務執行在當前 task 結束以後,下一個 task 開始以前。

參考:

  1. The Node.js Event Loop, Timers, and process.nextTick()
  2. JavaScript 運行機制詳解:再談Event Loop
  3. The Node.js Event Loop, Timers, and process.nextTick()
  4. 深刻理解 JavaScript 事件循環(二)— task and microtask

更多專業前端知識,請上 【猿2048】www.mk2048.com
相關文章
相關標籤/搜索