你懂 JavaScript 嗎?#22 非同步:現在和以後

你所不知道的 JS

本文主要會談到

事件迴圈(Event Loop)

開發者會利用函式(function)的方式將程式碼切成一個個片段,而這些函式執行的時機是由一種排程機制所控管的-由「事件迴圈」來控制。事件迴圈是一種類似佇列(queue)的機制,用來管理何時執行哪一個函式。當要執行這個函式時,就將它放到事件迴圈中等候執行。例如:發送 ajax 後要執行某個 callback,於是告訴宿主環境在收到回應時將這個 callback 放到 queue 中準備執行。另外,每一次 loop 稱為一個 tick,每個 tick 會取出 queue 中的 job,也就是要執行的函式來執行。

排隊

一種很多人在排隊的 feel ~

如下所示,這是一個事件迴圈選取工作執行的虛擬碼。eventLoop 是一個佇列,存放準備要執行的工作,每次會從中取出一個工作來執行,而這個迴圈是常駐的,永遠沒有結束的時候。

var eventLoop = [];
var event;

while (true) {
  // 開始本次 tick ...
  if (eventLoop.length > 0) {
    // 取得工作
    event = eventLoop.shift();

    // 執行工作
    try {
      event();
    } catch (err) {
      reportError(err);
    }
  }
}

圖解。

事件迴圈(Event Loop)

備註

共時(Concurrency)

共時是指兩個以上的行程(process)在同一時間內同時執行。但對於事件迴圈佇列來說,工作是循序執行的,不會同一瞬間執行多個工作,共時的行程可以彼此沒有互動或有互動。

若有兩個行程 P1 與 P2 在一時間區段內交互執行,也就是有互動的狀況,P1 發出請求 Req i 後,P2 會依照回應 Res i 做出相對應的處理。也就是說,當 P1 與 P2 開始執行後,會有以下的請求和回應:

Req 1, Res 1, Req 2, Req 3, Res 3, Res 2

由此可知,請求和回應的順序是無法預期的。

關於共時的資源利用,有以下兩個議題:「值的共用」和「合作式共時」。

值的共用(Value Sharing)

使用一個全域變數(global variable)儲存結果,例如:「result」,P1 與 P2 一起存取 result。這裡要用一些小技巧來避免競爭狀態(race condition),例如:使用閘門(gate)或閂鎖(latch)。

共用一顆蘋果?

分享

備註

var isDone = false; // flag

if (!isDone) {
  isDone = true;
  // 執行工作
}

合作式共時(Cooperative Concurrency)

獨佔

目前執行的工作耗費太多時間,形成獨佔資源的狀態,導致其他工作必須長時間等待,解決方法就是將這個工作切割成片段,逐步完成。

獨佔

分散

將長時間佔用資源的工作切割成多個子工作,逐步完成,好處是讓其他工作能排入事件迴圈佇列。對使用者來說,有種工作很快被完成的錯覺。

multitasker

圖片來源:Vida Diamante: Como Administrar Bien Tu Tiempo

在下例中,發送一個 ajax request,再由 response 這個 callback 處理回傳結果。由於回傳後的結果 data 的資料量可能非常大,處理起來需要花費非常久的時間,因此先切割成兩份,前一千份資料放在 chunk 中,其餘資料仍放在 data 中,然後先對 chunck 做處理,待之後再使用 setTimeout(function() { ... }, 0 ) 這個 hack 的方式將剩餘的資料和其處理工作加入事件迴圈佇列中。

var res = [];

function response(data) {
  var chunk = data.splice(0, 1000);

  res = res.concat(
    chunk.map(function (val) {
      return val * 2;
    }),
  );

  if (data.length > 0) {
    setTimeout(function () {
      response(data);
    }, 0);
  }
}

ajax('http://some.url', response);

工作佇列(Job Queue)

這邊要特別說明,使用 setTimeout(function() { ... }, 0 ) 加入事件迴圈佇列並無法確定執行時間,只是將這個工作放到目前隊伍(工作佇列,job queue)的尾端,待下一個 tick 優先執行,這是一個代辦清單的概念,是一種插隊的行為。

console.log('A');

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

schedule(function () {
  console.log('C');

  schedule(function () {
    console.log('D');
  });
});

假設 schedule 是一個假想的事件迴圈佇列排程的 API,使用 setTimeout(function() { ... }, 0 ) 加入一個工作 B,B 會放到目前佇列的尾端,等待下一個 tick 優先執行。

得到 A -> C -> D -> B。

回顧

看完這篇文章,我們到底有什麼收穫呢?藉由本文可以理解到…

References


同步發表於2019 鐵人賽


You-Dont-Know-JS Event Loop javascript 2019鐵人賽 你所不知道的JS 你懂JavaScript嗎? 鐵人賽 You-Dont-Know-JS-Async-and-Performance 你懂 JavaScript 嗎?2019 iT 邦幫忙鐵人賽 系列文