事件循環(event loop)是一個在 JavaScript 常被提起的概念, 它存在於瀏覽器也存在於 Node.js 中, 由於它複雜的運行機制使人難以琢磨, 更是成爲面試時候的必考題目.javascript
本篇文章的內容源自我對網絡上的一些介紹 "事件循環" 的演講視頻中內容的總結, 其中大多數來自於 jsconf
. 在每一章中我都附上的視頻地址, 建議之間直接觀看視頻相於文字來講視頻更容易理解.css
https://youtu.be/8aGhZQkoFbQ
咱們都知道JavaScript是一個單線程的編程語言, 這意味着它只能有一個調用棧, 執行過程當中每次只能作一件事情.html
爲了充分理解, 咱們來例舉一個簡單的例子:java
function fun1() { return 'fun1' } function fun2() { return 'fun2' + fun1(); } function fun3() { console.log(fun2()); } fun3();
一旦代碼執行調用棧中函數壓入的順序以下:node
+----------+ +----------+ +----------+ | stack | | stack | | stack | | | | | | | | | | | | | | | | | | +------+ | | | | | | | fun3 | | | | | | | +------+ | | | | | | | | | | +------+ | | +------+ | | | | | fun2 | | | | fun2 | | | | | +------+ | | +------+ | | | | | | | | +------+ | | +------+ | | +------+ | | | fun1 | | | | fun1 | | | | fun1 | | | +------+ | | +------+ | | +------+ | | | | | | | +----------+ +----------+ +----------+
位於棧頂的函數(fun3)執行完成後調用棧會把他彈出:linux
+----------+ +----------+ +----------+ | stack | | stack | | stack | | | | | | | | | | | | | | | | | | | | 💥 | | | | | | | | | | | | | | | | | | +------+ | | | | | | | fun2 | | | 💥 | | | | +------+ | | | | | | | | | | | | +------+ | | +------+ | | | | | fun1 | | | | fun1 | | | ✨ | | +------+ | | +------+ | | | | | | | | | +----------+ +----------+ +----------+
調用棧被JavaScript引擎使用和監視, 因此當咱們在函數中拋出了一個錯誤可是沒有對應的 try/catch
語句的時候:程序員
function fun1() { throw new Error('Warning: Nuclear Missile Launched') // 拋出一個錯誤可是沒有對於的 try/catch 語句 } function fun2() { return 'fun2' + fun1(); } function fun3() { console.log(fun2()); } fun3();
能夠在控制檯中的報錯中看到調用棧的信息:web
可是調用棧也是脆弱的若是咱們建立一個沒法中斷調用函數, 那麼調用棧會瞬間爆炸💥這種行爲被稱爲 "棧溢出". 可喜可賀的是JavaScript會監視調用棧, 一旦調用棧出現 "棧溢出" JavaScript 會終止代碼的執行而且拋出錯誤:面試
function foobar() { foobar(); } foobar();
拋出 "棧溢出" 錯誤:ajax
在 Web 開發中咱們常常要會聽到要避免瀏覽器阻塞, "阻塞" 這個詞感受上有點像咱們常說的 "電腦卡住了" 中的 "卡".
實際上阻塞並無一個嚴格的定義, JavaScript 的運行速度是很快的, 執行幾個簡單的 "console.log" 你沒法體會到這其中所花費的時間, 若是調用棧中正在執行的函數花費了大量的時間, 咱們感受瀏覽器被卡住了, 體驗不流暢此時咱們說, 瀏覽器被阻塞了.
可是瀏覽器不只僅執行 JavaScript 代碼還會異步的加載外部資源文件, 這些加載的流程也能夠被 JavaScript 所控制, 例如咱們經過 JavaScript 發起一個同步的網絡請求:
var oReq = new XMLHttpRequest(); oReq.onload = function(){}; oReq.open("get", "https://xxx.com/", false); // false 表示同步發送請求(這種方式已經不在被推薦使用僅僅用於示例) oReq.send(); alert('running!'); // 只有請求完成後 alert 纔會執行
這個請求有可能須要 20ms 或者 300ms 甚至更長, 在請求的過程當中瀏覽器會一直等待請求完成甚至會中止頁面的渲染, 咱們能夠明顯的感覺到瀏覽器卡住了, 這是典型的阻塞.
瀏覽器將全部可能花費大量時間等待的操做都提供了對應的異步接口, 這種解決方式被稱爲 "異步函數" 或者 "回調函數" 或者 "異步回調" 等.
一個典型的例子以下:
console.log('hello'); setTimeout(function foobar(){ console.log('delay'); },1000); console.log('world');
咱們都知道這段代碼會輸出的順序是:
那麼瀏覽器究竟是如何解釋這段代碼的呢, 咱們能夠觀察瀏覽器的調用棧:
console.log('hello'); // 壓入棧中執行 console.log('hello'); // 執行完成棧彈出 setTimeout // 壓入棧中執行 setTimeout // 執行完成棧彈出 console.log('world'); // 壓入棧中執行 console.log('world'); // 執行完成棧彈出 // -------1000ms事後------ foobar // 壓入棧中執行 console.log('delay'); // 壓入棧中執行 console.log('delay'); // 執行完成彈出 foobar // 執行完成彈出
最神奇的事情出現了 setTimeout
執行完成後就被調用棧彈出了, 可是不知何故 1000ms 後 setTimeout
中的 foobar
被神奇的喚醒了, 這是怎麼回事?
在 JavaScript 中執行元素綁定事件, 使用 setTimeout
設置一個延時執行, 或者發起一次網絡請求. 這些都是web開發者的屢見不鮮, 對於咱們來講這些內容就是 JavaScript 的一部分了, 可是實際上這些內容並無在 ECMAScript 制定的標準中, 包括咱們討論的 "事件循環" 機制它也沒有存在於規範中也就是說這個機制是獨立於 JavaScript 引擎的功能.
這些API被稱爲 webapi
它們有本身的規範和實現與 JavaScript 這門語言沒有關係, 在這份來自於MDN的頁面上列舉了大部分的API.
咱們以前提到了 JavaScript 因爲其單線程的特性只能在同一時間執行同一間事情, 這是正確的, 可是瀏覽器不只僅擁有解釋 JavaScript 腳本的引擎, 還有一堆其餘的程序來處理諸如 DOM
XmlHttpRequest
setTimeout
這些任務.
這些程序各司其職完成本身負責的部分, 它們是獨立於 JavaScript 引擎以外的內容, 這些程序可能運行在獨立線程上或者進程上它們經過webapi
來和 JavaScript引擎進行通訊, 因此咱們須要經過 "回調" 的方式進行異步編程.
而 事件循環 用於管理這些異步任務在合適的時機執行.
setTimeout
做爲 webapi
典型的例子, 咱們來觀察一下它是如何運行的, 首先 setTimeout
一旦被執行便將函數鉤子移交給對應的 webapi
而且開始計時:
+--------------------------------+ +---------------------+ | | |webapis | | | | +----------------+ | | setTimeout(function foobar() { | | | | | | console.log('delay') | +-> | | timer-0 -- cb()| | | },0); | | | | | | | | +----------------+ | | | | | +--------------------------------+ +---------------------+
因爲咱們的倒計時是0, 因此計時會當即完成, webapi
將函數鉤子移交給 任務隊列 .
+--------------------------------+ +----------+ | | |webapis | | | | | | setTimeout(function foobar() { | | | | console.log('delay') | | | +-+ | },0); | | | | | | | | | | | | | | +--------------------------------+ +----------+ | | +-------------------------------------------------+ | | task queue | | | +--------------+ | | | | | | | | | cb() | | <-+ | | | | | +--------------+ | | | +-------------------------------------------------+
接下來終於輪到 事件循環 上場了, 事件循環完成一件很是簡單的工做, 它判斷若是:
那麼就將這個任務移入到調用棧中執行:
+--------------------------------+ +------------------+ | | | stack | | | | +--------------+ | | setTimeout(function foobar() { | | | | | | console.log('delay') | | | cb - foobar | | | },0); | | | | | | | | +--------+-----+ | | | | ^ | +--------------------------------+ | | | event loop ⏳ | | | +----+---------------------------+ | | | | | | | | | | | | | | | | +-- task ------> push -------------------+ | | | | | +--------------------------------+ +------------------+
而後往復循環這個過程, 這就是事件循環的基本流程.
如今咱們來提出一個 ajax
的例子, 嘗試一下你能說出他的執行流程嗎:
console.log('hello'); $.get('https:www.google.com',function foobar(){ console.log('callback') }); console.log('world');
他的執行流程以下:
webapi
中的 XHR
發起了網絡請求, 並保存其函數鉤子webapi
將函數鉤子移動到任務隊列中事件循環--> 檢查調用棧是否爲空 檢查任務隊列中是否有任務 (事件循環一直存在並不是在只在此刻進行檢查)
https://youtu.be/cCOL7MC4Pl0
在上一節中咱們提到了 JavaScript 是單線程的若是花費大量的時間運行 JavaScript 那麼就會阻塞瀏覽器, 致使瀏覽器沒法完成其餘工做, 而且初次瞭解了 "事件循環" 是如何解決此問題的. 在這一節中咱們會更加了解另一個話題 "事件循環" 與頁面渲染之間的關係.
實際上事件循環的做用範圍可能超乎你的想象, 全部的DOM事件實際上都是受到了 "事件循環" 控制的, 除此之外還包括包括網絡請求, IO操做等等. 由於在背後這些事件的觸發者實際上都是將與事件有關的信息放入到了 "任務隊列" 中真正讓這些內容被執行的其實是 "事件循環", 還記得 "事件循環" 是如何工做的嗎?
當下列條件成立時, "事件循環" 會將最近的任務移送到 "調用棧" 中:
可是瀏覽器不只能夠執行 JavaScript 還會能夠處理頁面渲染, 咱們知道若是進行大量的 JavaScript 計算會阻塞頁面渲染, 這裏到底有何種聯繫? 在此以前咱們先來了解一下基本的渲染概念.
咱們在頁面中動態的修改樣式界面咱們能夠看到實時的效果, 因此給咱們一種修改樣式是同步的操做的錯覺, 實際上瀏覽器在背後進行了優化, 重複多此的樣式操做會被進行合併而後由瀏覽器決定一個合適的時機而後統一更新:
element.style.transition = "transform 1s"; element.style.transform = "translateX(100px)"; element.style.transform = "translateX(500px)";
應用了樣式的元素並不會橫向在 100px 和 500px 之間來回移動而是直接移動到了 500px, 瀏覽器拋棄了舊的無用的樣式修改.
因此說修改樣式並非實時的, 這種批量更新樣式的機制致使它和 "事件循環" 之間產生了一些微妙的關聯, 在瞭解這些關聯前咱們先來了解一下瀏覽器的基本渲染機制.
元素樣式決定渲染的結果, 從一堆代碼轉爲可視化的界面經歷了許多環節, 這裏咱們來簡單的瞭解一下其中的幾個關鍵步驟:
在下面的這個例子中使用了一個 video
來表示頁面進行動態的持續的頁面渲染, 這些嵌入的內容能夠在不受 JavaScript 的干擾下影響頁面的顯示, 當點擊按鈕的時候 JavaScript 進入死循環:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width"> <title>test</title> </head> <body> <video src="https://vjs.zencdn.net/v/oceans.mp4" controls autoplay></video> <button id="button">while true</button> <script> document.getElementById('button').addEventListener('click',()=>{ while(true); }); </script> </body> </html>
當咱們點擊了按鈕的時候:
while true
執行此時渲染工做在等待 JavaScript 執行完成, 可是 JavaScript 進入了無限循環中因此渲染工做就一直在等待中永遠不會獲得完成.
可是頁面的渲染卻受到了來自事件循環中的阻塞, 爲何會這樣?
解釋這種行爲的一個好的方式就是: 咱們不妨把頁面的渲染過程也視爲 "任務隊列" 中的一個任務, 這個任務的建立者就是瀏覽器自己,它在一個合適的時機把渲染任務放入到任務隊列中等待執行, 可是因爲代碼阻塞致使頁面沒法及時更新.
下列代碼會形成阻塞嗎:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width"> <title>test</title> </head> <body> <video src="https://vjs.zencdn.net/v/oceans.mp4" controls autoplay></video> <button id="button">while true</button> <script> function loop(){ setTimeout(loop,0); } loop(); </script> </body> </html>
這段代碼不只不會形成阻塞甚至不會形成棧溢出, 每次調用 loop
函數會向 "任務隊列" 添加一個任務而後 loop
就會被彈出調用棧, 此時的調用棧就被清空了. 而負責控制任務隊列的事件循環只有在調用棧爲空的時候才能繼續執行任務, 也就是說調用棧永遠不會累加.
其次任務的執行並不影向任務隊列添加內容, 在調用 setTimeout(loop,0)
後 "事件循環" 繼續工做處理那些已經被填入到任務隊列中在此以前的填入的其餘事件以及頁面的渲染, 直到 "任務隊列" 中再次執行有關 loop
的任務.
試想一下你在頁面上製做了一個動畫效果使用以下代碼:
function animate(){ // 修改樣式 } setInterval(animate,1000/60);
你但願動畫能夠達到 60fps 因此向 setInterval
傳入了 1000/60
期待它能夠每秒執行 60 次動畫函數.
可是因爲 setInterval
並不精確在一幀中可能執行了多此, 也可能一次也沒有執行, 或者執行了一個耗時的任務致使瀏覽器沒法在一幀中進行渲染操做.
咱們但願每一幀中至少有一次渲染過程, 可是隨機的任務執行會打亂理想中有規律的渲染過程, 致使渲染操做不能平均分佈到每幀中:
而瀏覽器自己的渲染其實是很是智能且節約計算. 例如頁面渲染頻率自動和屏幕刷新率調整到一致, 當頁面靜止或者不可視的時候頁面會中止渲染, 而使用 setInterval
等很難完美的和頁面渲染過程相結合.
一個解決問題的辦法就是使用 requestAnimationFrame
.
window.requestAnimationFrame() 告訴瀏覽器——你但願執行一個動畫,而且要求瀏覽器在下次重繪以前調用指定的回調函數更新動畫
使用 requestAnimationFrame
咱們可使用瀏覽器的渲染邏輯將本來雜亂的渲染過程變得有序起來, 讓瀏覽器決定什麼時候進行渲染, 對於動畫渲染這再好不過了, 如今有關動畫的任務都被排列到了渲染任務的前面:
有關宏任務的概念在前面咱們已經涉及到了, 在瀏覽器中如下的幾個異步 API 是宏任務相關:
而微任務是一個簡單的概念, 咱們從微任務的設計歷史來解釋微任務爲何這樣執行.
好久前W3C給瀏覽器制定了一些API, 這些API用於監聽DOM的變化:
element.addEventListener("DOMNodeInserted", function (ev) { // ... }, false);
可是這個API有着嚴重的性能問題, 只要修改元素的屬性對應的事件就會被觸發, 對同一個屬性修改100次就會觸發100次的事件, 另外事件具備冒泡的特性子元素的修改也會致使父元素觸發該事件:
let i = 100; while(i--){ const span = document.createElement('span'); element.appendChild(span); // 追加元素觸發事件一次 span.textContent = 'Test'; // 修改追加的元素的屬性又觸發一次事件 }
結果就是不管你在 DOMNodeInserted
回調中寫多麼簡單的代碼, 複雜的DOM操做致使事件就會被密集調用大大下降性能, 因此這個API被廢棄了.
監聽DOM修改的需求依然存在, 可是咱們但願這個接口的表現就和渲染同樣將DOM修改進行合併後只觸發一次, 這個規範在DOM3中被推出他就是 MutationObserver
, 從而引入了 "微任務" 和 "微任務隊列" 這個概念.
微任務是異步的咱們用個 Promise
來舉例:
Promise.resolve().then(()=>console.log('hello world')); console.log('foobar');
輸出:
foobar hello world
foobar
先於 hello world
輸出這點證實了它. 雖然他是異步的但這不表明他必須遵循 "事件循環" 和 "渲染" 制定的規則. 相反 "微任務" 有本身的玩法.
一個典型的特徵就是只有微任務隊列清空後微任務纔算執行完成, 咱們把以前的 "事件循環" 例子改成微任務版本:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width"> <title>test</title> </head> <body> <video src="https://vjs.zencdn.net/v/oceans.mp4" controls autoplay></video> <button id="button">while true</button> <script> document.getElementById('button').addEventListener('click',()=>{ function loop(){ Promise.resolve().then(loop); } loop(); }); </script> </body> </html>
結果就是當點擊按鈕後頁面渲染會中止瀏覽器進入到阻塞中, 緣由很簡單 "微任務隊列" 中永遠有任務因此瀏覽器一直在等待 "微任務隊列" 清空而後一直執行微任務, 不幸的是這是一個無盡的任務隊列因此它永遠沒法執行完.
微任務的另一個特性就是 JavaScript 調用棧一旦被清空, 微任務隊列中的任務執行會先於其餘隊列, 請觀察下面的例子:
const button = document.getElementById('button'); button.addEventListener('click',()=>{ Promise.resolve().then(()=>console.log('Microtask 1')); console.log('task 1'); }); button.addEventListener('click',()=>{ Promise.resolve().then(()=>console.log('Microtask 2')); console.log('task 2'); });
點擊按鈕後輸出順序:
task 1 Microtask 1 task 2 Microtask 2
按鈕點擊後的執行流程以下:
"事件循環" 將第一個點擊事件中的匿名函數壓入調用棧
console.log('task 1')
console.log('Microtask 1')
"事件循環" 將第二個點擊事件中的匿名函數壓入調用棧
console.log('task 2')
console.log('Microtask 2')
不過若是要把這個例子稍稍修改一下狀況卻略有不一樣:
const button = document.createElement('button'); button.addEventListener('click',()=>{ Promise.resolve().then(()=>console.log('Microtask 1')); console.log('task 1'); }); button.addEventListener('click',()=>{ Promise.resolve().then(()=>console.log('Microtask 2')); console.log('task 2'); }); button.click();
此次咱們手動觸發 click
事件, 輸出結果以下:
task 1 task 2 Microtask 1 Microtask 2
此次的執行流程爲:
button.click()
被壓入調用棧執行同步觸發 'click' 事件並將第一個事件回調壓入調用棧
console.log('Microtask 1')
console.log('task 1')
button.click
並未執行完成還在調用棧中同步觸發 'click' 事件並將第二個事件回調壓入調用棧
console.log('Microtask 2')
console.log('task 2')
button.click
從調用棧中彈出console.log('Microtask 1')
console.log('Microtask 2')
在瀏覽器中如下的 API 是微任務的任務源:
https://youtu.be/zphcsoSJMvM
回到 ms-dos
和 Apple os
的時代那時候的操做系統使用命令行界面, 計算機CPU只有一個核心, 操做系統同一時間下只能執行一件事情.經過操做界面告訴操做系統你要運行一個應用程序, 此時系統會中止運行並運行那個程序, 當應用程序執行完後又把執行權力交由操做系統.
這種設計有着很是大的限制, 你沒法同時執行多件事情, 因而一種被稱做協做多任務(cooperative multitasking)的機制出現了. 這種設計下你能夠同時運行多個程序.
可是這種機制出現後表現各大操做系統的實現也不是十分完美, 在當時的 Mac OS
和 windows
操做系統中這種多程序執行的實現交由應用程序決定, 若是程序沒有編寫對應的代碼那麼這個程序會一直佔用CPU資源, 若是一旦程序崩潰甚至會牽扯到系統, 致使系統崩潰.
後來這種機制被改成了搶佔式多任務(preemptive multitasking), 運行哪一個程序由操做系統決定, 在切換應用程序的時候他會把正在運行中的程序暫停而後保留其狀態存儲到其餘位置中, 而後加載另一個程序. 這項機制最初引用到了面向服務器的 Unix 系統, 在隨後的時間裏才應用到了使用 NT 內核的 windows2000 和同時期的 Mac OS 1004 的我的電腦中.
此時AMD剛剛發佈了它的多核CPU, 爲了充分利用多核CPU的性能, 均衡多線程(symmetric multi threading)技術誕生了, 該技術的實際應用被 intel 首先採用並從新命名爲超線程(hyper threading). 該技術的主要原理是: CPU在執行任務的時候並不是一直滿載執行, 這裏有不少空閒資源能夠利用, 而超線程容許一個CPU核心能夠同時處理多件事情充分的利用CPU空閒資源.
在上文中咱們提到了兩個基本的概念 "任務" 和 "線程", 實際上任務就是咱們常說的 "進程", 線程和進程的概念咱們就在這裏不提了, 咱們須要提到的一點就是線程的 "競態". 線程的執行是並行的線程間共享內容, 當兩個線程操做同一個數據的時候會出現這種問題.
假設咱們有兩個線程線程A向全局變量寫入一個數據,線程B讀取對應的全局變量,因爲兩個線程都是並行的因此線程A可能在線程B讀取前進行了寫入, 也有可能線程B先讀取後線程A再寫入, 每次運行都會獲得不一樣的結果. 這讓程序充滿了不肯定性也會致使不少bug, 許多語言都提供了線程安全的操做來避免問題, 不過即便是經驗豐富的程序員每每也得仔細思考才能設計出線程安全的程序.
對於Node來講解決的方式很是簡單直接, 咱們使用單線程模型, 不容許你使用多線程模型(Node 11中添加了實驗中的多線程支持).
Node.js 官網中有篇專門介紹事件循環的文章, 也是這節的核心, 這篇文章已經被翻譯完成 👉訪問鏈接. 原本打算放到這這篇文章中來的可是這樣作致使本文太長了, 因此就移除出去了, 是一篇十分重要的文章, 對於理解 Node.js 中的事件循環相當重要.
https://youtu.be/gl9qHml-mKc
JavaScript 是單線程的這沒有問題, 由於在 Node 中全部的 "JavaScript 腳本", "V8 引擎", "事件循環", 都運行在一個線程中這個線程被稱爲 "主線程".
可是這不意味着 Node 自己是單線程的由於 Node 還有其餘部分. Node 的源碼中還包括了 C++ 代碼, C++部分擁有操做線程的能力, 這取決於你調用 JavaScript API的方式. 例如若是你調用了一個 Node 的 API, 這個 API 背後是由 C++ 代碼提供支持的. 若是你同步的調用那麼 C++ 代碼會在主線程上執行. 若是你調用一個異步的 Node 接口, 那麼 C++ 有可能會使用額外的線程來執行這個任務.
因此使用同步 API 那麼 Node 就沒有機會利用多線程的並行計算的特性來提高性能, 因此在任什麼時候候都推薦使用異步的接口, 這樣能夠利用 Node 內部的線程機制進行優化執行效率.
在默認狀況下 Node 會使用線程池線程池的容量爲 4 (能夠經過環境變量進行修改), 當須要線程的任務超過4個後, Node 會將任務放入到任務隊列中. 一旦空閒線程出現 Node 會將任務從隊列中取出放入到線程中執行.
圖片:在 windows10 上使用命令行剛剛啓動的 Node 就使用了12個線程:
有一些任務不依賴線程池而是依賴操做系統提供的接口, 例如 http.request
背後 C++ 會盡量的調用系統提供的異步接口 epoll(linux) kqueue(mac os), GetQueuedCompletionStatusEx(Windows)來將任務委託給操做系統去完成.
下列的列表中例舉了異步 API 背後的運行機制:
Kernel Async
tcp/udp
sockets, serversThread Pool
Signal Handler(posix only)
Wait Thread(windows only)
不少人認爲 Event loop 是獨立的它運行在一個單獨的線程中, 可是實際上 Event Loop 做爲 JavaScript 部分的內容是和 JavaScript 同樣運行在主線程上的.
若是你看過 Node.js 官方介紹事件循環的文章你就會知道(文章地址), 事件循環並非簡單的棧或者隊列的概念, 而是多個 "階段" 的集合, 在不一樣的 "階段" 中用於保存任務的數據結構和執行邏輯是不一樣的.
二者的不一樣點主要在事件循環的執行機制上:
此外 Node 還有特殊的 process.nextTick
, 該 API 被視爲微任務源之一, 可是執行方式和瀏覽器中的方式不一樣. 在 Node11
後 Node 端的微任務執行效果開始和瀏覽器端趨同.
Node 和瀏覽器共有 setTimeout
和 Promise
這兩個接口, 一個被視爲宏任務源另外一個被視爲微任務源, 咱們使用這兩個 API
來作個小測試, 測試用例以下:
setTimeout(() => { console.log('setTimeout - 1'); setTimeout(() => { console.log('setTimeout - 1 - 1') }); new Promise(resolve => resolve()).then(() => { console.log('setTimeout - 1 - then') new Promise(resolve => resolve()).then(() => { console.log('setTimeout - 1 - then - then') }); }); }); setTimeout(() => { console.log('setTimeout - 2'); setTimeout(() => { console.log('setTimeout - 2 - 1') }); new Promise(resolve => resolve()).then(() => { console.log('setTimeout - 2 - then') new Promise(resolve => resolve()).then(() => { console.log('setTimeout - 2 - then - then') }); }); });
在瀏覽器端輸出以下:
setTimeout - 1 setTimeout - 1 - then setTimeout - 1 - then - then setTimeout - 2 setTimeout - 2 - then setTimeout - 2 - then - then setTimeout - 1 - 1 setTimeout - 2 - 1
解釋:
// 兩個 setTimeout 中的任務都已經被到任務隊列中. ------------- // 執行任務 - 1 setTimeout - 1 - setTimeout 添加了一個新的任務 - 3 - Promise 添加了一個新的微任務 // 第一個任務結束, 發現微任務存在, 執行微任務 setTimeout - 1 - then - Promise 添加了一個新的微任務 // 微任務執行完成, 發現新的微任務, 執行微任務 setTimeout - 1 - then - then // 微任務執行完成 // 執行任務 - 2 setTimeout - 2 - setTimeout 添加了一個新的任務 - 4 - Promise 添加了一個新的微任務 // 第一個任務結束, 發現微任務存在, 執行微任務 setTimeout - 2 - then - Promise 添加了一個新的微任務 // 微任務執行完成, 發現新的微任務, 執行微任務 setTimeout - 1 - then - then // 微任務執行完成 // 執行任務 3 setTimeout - 1 - 1 // 執行任務 4 setTimeout - 2 - 1
Node10 執行以下:
// 進入 timer 階段 ------------- // 執行任務 - 1 setTimeout - 1 - setTimeout 添加了一個新的任務 - 3 - Promise 添加了一個新的微任務 // 執行任務 - 2 setTimeout - 2 - setTimeout 添加了一個新的任務 - 4 - Promise 添加了一個新的微任務 // timer 階段結束 // 執行微任務 setTimeout - 1 - then - Promise 添加了一個新的微任務 // 執行微任務 setTimeout - 2 - then - Promise 添加了一個新的微任務 // 執行微任務 setTimeout - 1 - then - then // 執行微任務 setTimeout - 2 - then - then // 微任務處理完成 // 進入其餘階段開始循環 -> 直到再次進入 timer 階段 // 執行任務 3 setTimeout - 1 - 1 // 執行任務 4 setTimeout - 2 - 1
正如以前所說 Node11 後執行邏輯發生了變化執行邏輯向瀏覽器靠攏, 這裏給出 Node12 中執行相同代碼的輸出:
setTimeout - 1 setTimeout - 2 setTimeout - 1 - then setTimeout - 2 - then setTimeout - 1 - then - then setTimeout - 2 - then - then setTimeout - 1 - 1 setTimeout - 2 - 1
另外咱們都知道 Node 有一個 process.nextTick
, 修改以前的代碼後:
setTimeout(() => { console.log('setTimeout - 1'); setTimeout(() => { console.log('setTimeout - 1 - 1') }); process.nextTick(() => { console.log('setTimeout - 1 - nextTick') process.nextTick(() => { console.log('setTimeout - 1 - nextTick - nextTick') }); }); }); setTimeout(() => { console.log('setTimeout - 2'); setTimeout(() => { console.log('setTimeout - 2 - 1') }); process.nextTick(() => { console.log('setTimeout - 2 - nextTick') process.nextTick(() => { console.log('setTimeout - 2 - nextTick - nextTick') }); }); });
在 Node10 中輸出以下:
setTimeout - 1 setTimeout - 2 setTimeout - 1 - nextTick setTimeout - 2 - nextTick setTimeout - 1 - nextTick - nextTick setTimeout - 2 - nextTick - nextTick setTimeout - 1 - 1 setTimeout - 2 - 1
咱們能夠看到輸出和結果和使用 Promise
別無二致, 另外在 Node12 中的輸出以下:
setTimeout - 1 setTimeout - 1 - nextTick setTimeout - 1 - nextTick - nextTick setTimeout - 2 setTimeout - 2 - nextTick setTimeout - 2 - nextTick - nextTick setTimeout - 1 - 1 setTimeout - 2 - 1
結果和以前的使用 Promise
也是沒有區別的, 不過一樣身爲微任務源之一, 在 Node 中仍是有前後之分的:
setTimeout(() => { console.log('setTimeout - 1'); process.nextTick(() => { console.log('setTimeout - 1 - nextTick') process.nextTick(() => { console.log('setTimeout - 1 - nextTick - nextTick') }); }); new Promise(resolve => resolve()).then(() => { console.log('setTimeout - 1 - then') new Promise(resolve => resolve()).then(() => { console.log('setTimeout - 1 - then - then') }); }); });
Node10 和 Node12 輸出:
setTimeout - 1 setTimeout - 1 - nextTick setTimeout - 1 - nextTick - nextTick setTimeout - 1 - then setTimeout - 1 - then - then
能夠看出 process.nextTick
的優先級高於 Promise
的.
https://nodejs.org/en/docs/gu...https://nodejs.org/dist/lates...
https://www.dynatrace.com/new...
https://blog.csdn.net/Fundebu...
https://www.cnblogs.com/MuYun...
http://www.ruanyifeng.com/blo...
https://jsbin.com/dijodahawi/...