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

  • pre-notify
  • 術語
  • 爲何JS要設計成單線程的?
  • JS並不是只有一個線程,而只是主線程是單線程的
  • 異步任務以及事件循環
    • Node.js中のEventLoop
    • 微任務和宏任務
      • 其它的微任務
  • Q
      1. setTimeout,setImmediate誰先誰後?
      1. nextTick和promise.then誰快?
      1. nextTick和其它的定時器嵌套
      1. 定時器指定的回調函數必定會在指定的時間內執行嗎?
  • 關於瀏覽器模型

pre-notify

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

參考html

術語

synchronous:同步任務java

asynchronous:異步任務node

task queue/callback queue:任務隊列web

execution context stack:執行棧canvas

heap:堆後端

stack:棧api

macro-task:宏任務promise

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主線程不是平級的,主線程能夠控制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主線程從callback queue中不斷讀取事件到執行棧中執行的這種循環的過程又被稱之爲EventLoop,即事件循環。

Node.js中のEventLoop

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

  • 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中事件環六中狀態之間轉化時的貓膩,光是瀏覽器中的事件環也可能由於本輪循環的執行時間過長,長得比定時器指定的事件還長從而致使定時器的回調觸發被延誤。

關於瀏覽器模型

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

瀏覽器是多進程的,每一個進程管理着瀏覽器不一樣的部分,主要分爲如下幾種

  • 用戶界面:包括地址欄、前進/後退按鈕、書籤菜單等
  • 瀏覽器引擎:在用戶界面和呈現引擎之間傳送指令
  • 呈現引擎,又稱渲染引擎,在線程方面又稱爲UI線程,這是最爲核心的部分,So也被稱之爲瀏覽器內核
  • GPU:用於提升網頁瀏覽的體驗
  • 插件:一個插件對應一個進程(第三方插件進程)

其中渲染引擎內部有三個線程是咱們注重須要關注的

  • Networking:用於網絡調用,好比HTTP請求
  • Javascript解釋器:用於解析和執行Javascript代碼
  • UI Backend

其中js線程和ui線程是互斥的,

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

除此以外還有一些其它的線程,這也是咱們分發異步任務時用到的線程

  • 瀏覽器事件觸發線程
  • 定時觸發器線程
  • 異步HTTP請求線程

--- End ---

相關文章
相關標籤/搜索