JS/NodeJS中的非同步任務與事件環

Cris_冷崢子發表於2018-03-22
  • pre-notify
  • 術語
  • 為什麼JS要設計成單執行緒的?
  • JS並非只有一個執行緒,而只是主執行緒是單執行緒的
  • 非同步任務以及事件迴圈
    • Node.js中のEventLoop
    • 微任務和巨集任務
      • 其它的微任務
  • Q
      1. setTimeout,setImmediate誰先誰後?
      1. nextTick和promise.then誰快?
      1. nextTick和其它的定時器巢狀
      1. 定時器指定的回撥函式一定會在指定的時間內執行嗎?
  • 關於瀏覽器模型

pre-notify

本文主要根據網上資源總結而來,如有不對,請斧正。

參考

術語

synchronous:同步任務

asynchronous:非同步任務

task queue/callback queue:任務佇列

execution context stack:執行棧

heap:堆

stack:棧

macro-task:巨集任務

micro-task:微任務

為什麼JS要設計成單執行緒的?

在進入正式的主題之前我們來探究探究這個歷史問題,嗯,還是有點價值的哈,因為JS若不是單執行緒的也就不會衍生出後來的非同步任務以及事件環了嘛。


js最開始時是跑在瀏覽器端的,主要的作用是與使用者互動(接收使用者輸入並給出自定義的響應)以及操作DOM(各種特效,控制輸入輸出)。

So網上有一種說法,舉了個栗子說,兩個執行緒,有一個執行緒在新增一個dom元素 a,還有一個執行緒在刪除一個dom元素a,那麼瀏覽器就需要決策該聽誰的,這樣的話就增加了語言設計的複雜性。

嗯。。。如果這例子以及說法還不能令你信服,我覺得你可以再想想javascript當初開發時的情景

其實我覺的這也是很重要的一個原因之一,嗯,這是我比較能代入的,那就是

我大javascript當初10天就給弄出來了。。。so,你還想咋地?

JS並非只有一個執行緒,而只是主執行緒是單執行緒的

h5有一個新api,它叫webworker,利用它,能幫助我們建立子執行緒

//index.html
let worker = new Worker('./worker.js');
//把內容發給 工作執行緒,工作執行緒可以把結果丟回來

worker.postMessage('向你僱傭的工人傳送指令');

worker.onmessage = function(e){
  console.log(e.data); //資料在data屬性下
}

//worker.js
window.onmessage = function(e){
	console.log(e);
  this.postMessage('你的工人告訴你他收到命令開始幹活了!');
};
複製程式碼

JS/NodeJS中的非同步任務與事件環


注意: 他和js主執行緒不是平級的,主執行緒可以控制webworker,webworker不能操作dom,不能獲取document以及window。

非同步任務以及事件迴圈

單執行緒雖然簡單,不容易出錯,但是有一個問題,這貨一直是一個人在幹活,假若涉及到讀取寫入這種I/O行為,那麼不僅CPU資源是妥妥的浪費(webworker的出現其實可能是為了嘗試解決這部分問題),我們後面的程式碼還要等待它讀寫完畢才能執行,這就是所謂的站著那撒不拉那撒了- -!

So,為了解決這個問題,Javascript將任務的執行方式分為兩種:同步/synchronous非同步/asynchronous,遇到像上面那種需要長時間等待的I/O操作,我們就將它作為一個非同步任務分發出去,等待它執行完畢後再通知我們。

我們說遇到非同步任務會分發出去,那麼分發分發究竟分發給了誰呢?

在瀏覽器核心中,除了JS執行緒使用者介面後端/UI Backend執行緒,還有一些其它的執行緒,比如說瀏覽器事件觸發執行緒定時器觸發執行緒非同步HTTP請求執行緒,而非同步任務就是分發給這些執行緒來處理的。(如果有不大瞭解這方面的同學可以檢視本文的最後部分)

當這些非同步任務在對應的執行緒中處理完成得到結果後,這些任務的回撥就會被加入到callback queue佇列當中。另一方面,JS主執行緒的執行棧中一旦所有同步程式碼執行完畢後就會開始不停的檢測callback queue,只要佇列中存在任務,就會被提取到執行棧中執行。

下圖是網上流傳很廣的一張示意圖

JS/NodeJS中的非同步任務與事件環

其中JS主執行緒從callback queue中不斷讀取事件到執行棧中執行的這種迴圈的過程又被稱之為EventLoop,即事件迴圈。

Node.js中のEventLoop

node中的事件環和瀏覽器中的是不一樣的,node的事件環分為六個階段,每個階段都一有一個callbcak queue,只有當一個階段的queue清理乾淨後才會進入到下一個階段。

JS/NodeJS中的非同步任務與事件環

  • timer(計時器),執行setTimeout以及setInterval的回撥
  • I/O callbcacks,處理網路、流、tcp的callbcak以及錯誤
  • idle,prepare node內部使用
  • poll(輪詢),會等待I/O執行直到得到cb
  • check,處理setImmediate回撥
  • close callbcaks,處理關閉的回撥例如socke.on('close')

微任務和巨集任務

非同步任務主要分為兩種,

一種稱之為macro task,即巨集任務,像setTimeout、I/O讀寫、AJAX這類耗時灰常長的。

另外一種則稱之為micro task,即微任務,例如nextTickpromise。(即使定時器的delayt時間設定為0,也是巨集任務,會在本輪的微任務執行完畢後再執行)

巨集任務和微任務的區別在於,微任務是會被加入本輪迴圈的,而巨集任務都是在次輪迴圈中被執行。

本輪迴圈是指什麼呢?JS主執行緒會從任務佇列中提取任務到執行棧中執行,每一次執行都可能會再產生一個新的任務,對於這些任務來說這次執行到下一次從任務佇列中提取新的任務到執行棧之前就是這些新生任務的本輪

[danger] 注意: 在node中微任務的觸發時機是在進入事件環之前以及當狀態轉換的時才會觸發,這意味著如果一個狀態中的callbcak queue中的cb還沒有全部清空完畢,那麼微任務並不會像瀏覽器中一樣加入本輪後在一個回撥執行完畢後就會立即執行,而會等待queue清空後才執行。

其它的微任務

  • MutationObserve(不相容的),
  • MessageChannel

Q

1. setTimeout,setImmediate誰先誰後?

setTimeout(function(){
console.log('Timeout');
})
setImmediate(function(){
console.log('Immediate');
})
複製程式碼

網上有有一種說法,因為setTimeout雖然在node事件迴圈中的第一個階段,但setTimeout即使將delay設定為0也會有1ms+(node做不到那麼精確),那麼當第一次事件迴圈時,setTimeout可能還沒有準備好,就將會讓setImmediate先執行。

但,真會發生這種情況嗎?

嗯。。。我沒事點了幾十下,全是setTimeout。

另外還有如下一種情況,一定是setImmediate會先走的

fs.readFile('./1.txt',function(){
    console.log('fs');
    setTimeout(function(){
    	console.log('timeout');
    });
    setImmediate(function(){
    	console.log('setImmediate');
    });
})
複製程式碼

因為當fs的I/O回撥執行執行時是處於事件迴圈中的poll階段,而下一個階段為check是存放setImmediate的階段。

2. nextTick和promise.then誰快?

nextTick快,就是這麼設計的

3. nextTick和其它的定時器巢狀

setImmediate(function(){
  console.log(1);
  process.nextTick(function(){
    console.log(4);
  })
})
process.nextTick(function(){
  console.log(2);
  setImmediate(function(){
    console.log(3);
  })
})

<<<
2134
複製程式碼

原因在於nextTick在node中的執行實際和瀏覽器中不完全一樣,雖然它們在第一次進入事件環時都會先執行,但如果後續還有nextTick加入,node中只會在階段轉換時才會去執行,而瀏覽器中則是一有nextTick加入就會立即執行。

造成這樣區別的原因在於,node中的事件環是有6種狀態的,每種狀態都是一個callbcak queue,只有當一個狀態的callback queue中存放的回撥都清空後才會執行nextTick。

4. 定時器指定的回撥函式一定會在指定的時間內執行嗎?

不一定,先不說node中事件環六中狀態之間轉化時的貓膩,光是瀏覽器中的事件環也可能因為本輪迴圈的執行時間過長,長得比定時器指定的事件還長從而導致定時器的回撥觸發被延誤。

關於瀏覽器模型

嗯,先上圖。。也是很火的一張

JS/NodeJS中的非同步任務與事件環

瀏覽器是多程式的,每個程式管理著瀏覽器不同的部分,主要分為以下幾種

  • 使用者介面:包括位址列、前進/後退按鈕、書籤選單等
  • 瀏覽器引擎:在使用者介面和呈現引擎之間傳送指令
  • 呈現引擎,又稱渲染引擎,線上程方面又稱為UI執行緒,這是最為核心的部分,So也被稱之為瀏覽器核心
  • GPU:用於提高網頁瀏覽的體驗
  • 外掛:一個外掛對應一個程式(第三方外掛程式)

其中渲染引擎內部有三個執行緒是我們注重需要關注的

  • Networking:用於網路呼叫,比如HTTP請求
  • Javascript直譯器:用於解析和執行Javascript程式碼
  • UI Backend

其中js執行緒和ui執行緒是互斥的,

當js執行的時候可能ui還在渲染,那麼這時ui執行緒會把更改放到佇列中 當js執行緒空閒下來 ui執行緒再繼續渲染

除此之外還有一些其它的執行緒,這也是我們分發非同步任務時用到的執行緒

  • 瀏覽器事件觸發執行緒
  • 定時觸發器執行緒
  • 非同步HTTP請求執行緒

--- End ---

相關文章