EventLoop 分享

什麼是Event loop?

ECMAScript規範沒有提到事件循環。相反,事件循環在HTML規範中有詳細說明,儘管事件循環是在HTML規範中定義的,可是在其餘環境中,好比Nodejs,也使用了它。javascript

HTML Standard中的定義

爲了協調事件,用戶交互,腳本,渲染,網絡等,用戶代理必須使用本節所述的event loop。 Events,Parsing,Callbacks,Using a resource,Reacting to DOM manipulation 事件,用戶交互,腳本,渲染,網絡這些都是咱們所熟悉的東西,他們都是由event loop協調的。觸發一個click事件,進行一次ajax請求,背後都有event loop在運做。html

瀏覽器的event loop

An event loop has one or more task queues. A task queue is a set of tasks.vue

一個 event loop有一個或多個任務隊列,一個任務隊列是一個多任務的集合。java

Task queues are sets, not queues, because step one of the event loop processing model grabs the first runnable task from the chosen queue, instead of dequeuing the first task.node

任務隊列是集合,而不是隊列,由於事件循環處理模型的第一步從選擇的隊列中獲取第一個可運行的任務,而不是退出第一個任務。web

The microtask queue is not a task queue.ajax

微任務隊列不是任務隊列。segmentfault

在規範的Processing model定義了event loop的循環過程:api

  • 一個event loop只要存在,就會不斷執行下邊的步驟:
1.在tasks隊列中選擇最老的一個task,用戶代理能夠選擇任何task隊列,
若是沒有可選的任務,則跳到下邊的microtasks步驟。
2.設置oldestTask成爲任務隊列中第一個可運行的任務,並將其從任務隊列中刪除。
5.運行oldestTask裏面的一系列腳本
7.從其任務隊列中刪除oldestTask
8.Microtasks: 執行microtasks任務檢查點。(也就是執行microtasks隊列裏的任務)
11.更新渲染(Update the rendering)...

複製代碼

event loop會不斷循環上面的步驟,歸納說來:

  1. event loop會不斷循環的去取tasks隊列的中oldestTask(可執行的)一個任務推入棧中執行。
  2. 執行棧中的oldestTask一步一步執行完後,出棧。
  3. 當執行棧爲空時,檢查微任務(microtask checkpoint),不爲空執行微任務
  4. 當微任務隊列最終爲空時,事件循環檢查是否須要UI呈現更新,若是須要,則從新呈現UI。
  5. 這將結束事件循環的當前迭代,該循環將回到開始並再次檢查宏任務隊列。

圖片出處: livebook.manning.com/book/secret…

microtask checkpoint

所作的就是執行microtask隊列裏的任務。何時會調用microtask checkpoint呢?promise

  • 每次回調以後,只要沒有其餘JavaScript處於執行中
  • 在event loop的第8 步(Microtasks: Perform a microtask checkpoint)執行checkpoint,也就是在每一個任務結束時,更新渲染以前。

演示

<div class="outer">
	<div class="inner"></div>
</div>
<script>

	// Let's get hold of those elements var outer = document.querySelector('.outer'); var inner = document.querySelector('.inner'); // Let's listen for attribute changes on the
	// outer element
	new MutationObserver(function() {
		console.log('mutate');
	}).observe(outer, {
		attributes: true
	});

	// Here's a click listener… function onClick() { console.log('click'); setTimeout(function() { console.log('timeout'); }, 0); Promise.resolve().then(function() { console.log('promise'); }); outer.setAttribute('data-random', Math.random()); } // …which we'll attach to both elements
	inner.addEventListener('click', onClick);
	outer.addEventListener('click', onClick);

</script>
複製代碼

點擊打印結果

click
promise
mutate
click
promise
mutate
timeout
timeout
複製代碼

此代碼出處和解釋> jakearchibald.com/2015/tasks-…

在node裏面的解釋

事件循環容許Node.js執行非阻塞的I/O操做(儘管JavaScript是單線程的),方法是儘量地將操做卸載到系統內核。

node 官網的 EventLoop

下圖簡要的概述了event loop的操做順序:

注:每個框表明event loop中的一個階段

每一個階段都有一個FIFO(先進先出)的回調隊列等待執行。雖然每一個階段都有其獨特之處,但整體而言,當event loop進入到指定階段後,它會執行該階段的任何操做,並執行對應的回調直到隊列中沒有可執行回調或者達到回調執行上限,然後event loop會進入下一階段。

因爲任何這些階段的操做可能產生更多操做,內核也會將新的事件推入到poll階段的隊列中,因此新的poll事件被容許在處理poll事件時繼續加入隊,這也意味着長時間運行的回調能夠容許poll階段運行的時間比計時器的閾值要長

階段概覽

翻譯:

  • timers:執行的是setTimeout()和setInterval()的回調
  • pending callbacks:執行延遲到下一個循環迭代的I/O回調
  • idle, prepare:僅內部使用
  • poll:檢索新的I/O事件;執行與I/O相關的回調(除了close回調、計時器調度的回調和setimmediation()以外,幾乎全部回調都執行);節點將在適當的時候在這裏阻塞
  • check:setImmediate回調在這裏觸發
  • close callbacks:好比socket.on('close', ...)

Between each run of the event loop, Node.js checks if it is waiting for any asynchronous I/O or timers and shuts down cleanly if there are not any.

在每次運行事件循環之間,Node.js檢查它是否在等待任何異步I/O或計時器,若是沒有,則乾淨地關閉。

setImmediate() vs setTimeout()

兩者的調用順序取決於它們的執行上下文。若是二者都在主模塊被調用,那麼其回調被執行的時間點就取決於處理過程的性能(這可能被運行在同一臺機器上的其餘應用影響)

好比說,若是下列腳本不是在I/O循環中運行,這兩種定時器運行的順序是不必定的

// timeout_vs_immediate.js
setTimeout(function timeout() {
  console.log('timeout');
}, 0);

setImmediate(function immediate() {
  console.log('immediate');
});
複製代碼
$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js
immediate
timeout
複製代碼

可是若是你把上面的代碼置於I/O循環中,setImmediate回調會被優先執行:

const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});
複製代碼
$ node timeout_vs_immediate.js
immediate
timeout

$ node timeout_vs_immediate.js
immediate
timeout
複製代碼

使用setImmediate()而不是setTimeout()的主要好處是:若是代碼是在I/O循環中調用,那麼setImmediate()老是優先於其餘定時器(不管有多少定時器存在)

setTimeout 的時延

When delay is larger than 2147483647 or less than 1, the delay will be set to 1.

在node下運行

setTimeout(()=> {
	console.log(1);
},1);

setTimeout(()=> {
	console.log(0);
},0);

# output 能夠看到1在前 0在後
1 2
0 4
複製代碼

在瀏覽器中,setTimeout()/setInterval() 的每調用一次定時器的最小間隔是4ms,這一般是因爲函數嵌套致使(嵌套層級達到必定深度),或者是因爲已經執行的setInterval的回調函數阻塞致使的。例如:

let i = 0
	let now = Date.now()

	function f(){
		let newD = Date.now()
		console.log(newD-now)
		now = newD

	}

	function cb() {
		f();
		i++
		if(i>10) return
	setTimeout(cb, 0);
	}

	setTimeout(cb, 0)
複製代碼

從輸出結果看也有小於4ms的,緣由就不知道了。

process.nextTick()

你可能已經注意到process.nextTick()不在上面的圖表中,即便它也是異步api。這是由於嚴格意義上來講process.nextTick()不屬於event loop中的一部分,它會忽略event loop當前正在執行的階段,而直接處理nextTickQueue中的內容。

回過頭看一下圖表,你在任何給定階段調用process.nextTick(),在繼續event loop以前,全部傳入process.nextTick()的回調都會被執行。這可能會致使一些很差的狀況,由於它容許你遞歸調用process.nextTick()從而使得event loop沒法進入poll階段,致使沒法接收到新的 I/O事件

爲何要使用process.nextTick()?

這裏有兩個主要的緣由

  1. 讓開發者處理錯誤、清除無用的資源或者在event
  2. loop繼續以前再次嘗試從新請求資源 有時須要容許回調在調用棧展開以後但在事件循環繼續以前運行

process.nextTick() 和 setImmediate()

如下截圖來源:《深刻淺出node.js》

參考

Q&A

  1. 深刻淺出講到的idle觀察者 和 node講到的idle階段有什麼關係?
  2. vue.nexttick 優先使用微任務promise實現的好處?
相關文章
相關標籤/搜索