JavaScript 異步、棧、事件循環、任務隊列

概覽

圖片描述
咱們常常會聽到引擎和runtime,它們的區別是什麼呢?javascript

  • 引擎:解釋並編譯代碼,讓它變成能交給機器運行的代碼(runnable commands)。
  • runtime:就是運行環境,它提供一些對外接口供Js調用,以跟外界打交道,好比,瀏覽器環境、Node.js環境。不一樣的runtime,會提供不一樣的接口,好比,在 Node.js 環境中,咱們能夠經過 require 來引入模塊;而在瀏覽器中,咱們有 window、 DOM。

Js引擎是單線程的,如上圖中,它負責維護任務隊列,並經過 Event Loop 的機制,按順序把任務放入棧中執行。而圖中的異步處理模塊,就是 runtime 提供的,擁有和Js引擎互不干擾的線程。接下來,咱們會細說圖中的:棧和任務隊列。html

如今,咱們要運行下面這段代碼:java

function bar() {
    console.log(1);
}

function foo() {
    console.log(2);
    far();
}

setTimeout(() => {
    console.log(3)
});

foo();

它在棧中的入棧、出棧過程,以下圖:
圖片描述node

任務隊列

Js 中,有兩類任務隊列:宏任務隊列(macro tasks)和微任務隊列(micro tasks)。宏任務隊列能夠有多個,微任務隊列只有一個。那麼什麼任務,會分到哪一個隊列呢?git

  • 宏任務:script(全局任務), setTimeout, setInterval, setImmediate, I/O, UI rendering.
  • 微任務:process.nextTick, Promise, Object.observer, MutationObserver.

瀏覽器的 Event Loop

瀏覽器的 Event Loop 遵循的是 HTML5 標準,而 NodeJs 的 Event Loop 遵循的是 libuv。 區別較大,分開講。github

咱們上面講到,當stack空的時候,就會從任務隊列中,取任務來執行。瀏覽器這邊,共分3步:web

  1. 取一個宏任務來執行。執行完畢後,下一步。
  2. 取一個微任務來執行,執行完畢後,再取一個微任務來執行。直到微任務隊列爲空,執行下一步。
  3. 更新UI渲染。

Event Loop 會無限循環執行上面3步,這就是Event Loop的主要控制邏輯。其中,第3步(更新UI渲染)會根據瀏覽器的邏輯,決定要不要立刻執行更新。畢竟更新UI成本大,因此,通常都會比較長的時間間隔,執行一次更新。vim

從執行步驟來看,咱們發現微任務,受到了特殊待遇!咱們代碼開始執行都是從script(全局任務)開始,因此,一旦咱們的全局任務(屬於宏任務)執行完,就立刻執行完整個微任務隊列。看個例子:segmentfault

console.log('script start');

// 微任務
Promise.resolve().then(() => {
    console.log('p 1');
});

// 宏任務
setTimeout(() => {
    console.log('setTimeout');
}, 0);

var s = new Date();
while(new Date() - s < 50); // 阻塞50ms

// 微任務
Promise.resolve().then(() => {
    console.log('p 2');
});

console.log('script ent');


/*** output ***/

// one macro task
script start
script ent

// all micro tasks
p 1
p 2

// one macro task again
setTimeout

上面之因此加50ms的阻塞,是由於 setTimeout 的 delayTime 最少是 4ms. 爲了不認爲 setTimeout 是由於4ms的延遲然後面才被執行的,咱們加了50ms阻塞。api

NodeJs 的 Event Loop

NodeJs 的運行是這樣的:

  • 初始化 Event Loop
  • 執行您的主代碼。這裏一樣,遇到異步處理,就會分配給對應的隊列。直到主代碼執行完畢。
  • 執行主代碼中出現的全部微任務:先執行完全部nextTick(),而後在執行其它全部微任務。
  • 開始 Event Loop

NodeJs 的 Event Loop 分6個階段執行:

┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

以上的6個階段,具體處理的任務以下:

  • timers: 這個階段執行setTimeout()setInterval()設定的回調。
  • pending callbacks: 上一輪循環中有少數的 I/O callback 會被延遲到這一輪的這一階段執行。
  • idle, prepare: 僅內部使用。
  • poll: 執行 I/O callback,在適當的條件下會阻塞在這個階段
  • check: 執行setImmediate()設定的回調。
  • close callbacks: 執行好比socket.on('close', ...)的回調。

每一個階段執行完畢後,都會執行全部微任務(先 nextTick,後其它),而後再進入下一個階段。

Links

相關文章
相關標籤/搜索