文章首次發表在 我的博客html
最近面試了不少家公司,這道題幾乎是必被問到的一道題。以前總以爲本身瞭解得差很少,可是當第一次被問到的時候,殊不知道該從哪裏開始提及,涉及到的知識點不少。因而花時間整理了一下。並不只僅是由於面試遇到了,而是理解JavaScript事件循環機制會讓咱們日常遇到的疑惑也獲得解答。html5
通常面試官會這麼問,出道題,讓你說出打印結果。而後會問分別說說瀏覽器的node的事件循環,區別是什麼,什麼是宏任務和微任務,爲何要有這兩種任務...node
本篇文章參考了不少文章,同時加上本身的理解,若是有問題但願你們指出。git
單線程:github
JavaScript的主要用途是與用戶互動,以及操做DOM。若是它是多線程的會有不少複雜的問題要處理,好比有兩個線程同時操做DOM,一個線程刪除了當前的DOM節點,一個線程是要操做當前的DOM階段,最後以哪一個線程的操做爲準?爲了不這種,因此JS是單線程的。即便H5提出了web worker標準,它有不少限制,受主線程控制,是主線程的子線程。web
非阻塞:經過 event loop 實現。面試
爲了更好地理解Event Loop,請看下圖(轉引自Philip Roberts的演講 《Help, I'm stuck in an event-loop》) vim
執行棧: 同步代碼的執行,按照順序添加到執行棧中promise
function a() {
b();
console.log('a');
}
function b() {
console.log('b')
}
a();
複製代碼
咱們能夠經過使用 Loupe(Loupe是一種可視化工具,能夠幫助您瞭解JavaScript的調用堆棧/事件循環/回調隊列如何相互影響)工具來了解上面代碼的執行狀況。瀏覽器
a()
先入棧a()
中先執行函數 b()
函數b()
入棧b()
, console.log('b')
入棧b
, console.log('b')
出棧b()
執行完成,出棧console.log('a')
入棧,執行,輸出 a
, 出棧事件隊列: 異步代碼的執行,遇到異步事件不會等待它返回結果,而是將這個事件掛起,繼續執行執行棧中的其餘任務。當異步事件返回結果,將它放到事件隊列中,被放入事件隊列不會馬上執行起回調,而是等待當前執行棧中全部任務都執行完畢,主線程空閒狀態,主線程會去查找事件隊列中是否有任務,若是有,則取出排在第一位的事件,並把這個事件對應的回調放到執行棧中,而後執行其中的同步代碼。
咱們再上面代碼的基礎上添加異步事件,
function a() {
b();
console.log('a');
}
function b() {
console.log('b')
setTimeout(function() {
console.log('c');
}, 2000)
}
a();
複製代碼
此時的執行過程以下
咱們同時再加上點擊事件看一下運行的過程
$.on('button', 'click', function onClick() {
setTimeout(function timer() {
console.log('You clicked the button!');
}, 2000);
});
console.log("Hi!");
setTimeout(function timeout() {
console.log("Click the button!");
}, 5000);
console.log("Welcome to loupe.");
複製代碼
簡單用下面的圖進行一下總結
爲何要引入微任務,只有一種類型的任務不行麼?
頁面渲染事件,各類IO的完成事件等隨時被添加到任務隊列中,一直會保持先進先出的原則執行,咱們不能準確地控制這些事件被添加到任務隊列中的位置。可是這個時候忽然有高優先級的任務須要儘快執行,那麼一種類型的任務就不合適了,因此引入了微任務隊列。
不一樣的異步任務被分爲:宏任務和微任務 宏任務:
微任務:
異步任務的返回結果會被放到一個任務隊列中,根據異步事件的類型,這個事件實際上會被放到對應的宏任務和微任務隊列中去。
在當前執行棧爲空時,主線程會查看微任務隊列是否有事件存在
當前執行棧執行完畢後時會馬上處理全部微任務隊列中的事件,而後再去宏任務隊列中取出一個事件。同一次事件循環中,微任務永遠在宏任務以前執行。
在事件循環中,每進行一次循環操做稱爲 tick,每一次 tick 的任務處理模型是比較複雜的,但關鍵步驟以下:
簡單總結一下執行的順序: 執行宏任務,而後執行該宏任務產生的微任務,若微任務在執行過程當中產生了新的微任務,則繼續執行微任務,微任務執行完畢後,再回到宏任務中進行下一輪循環。
深刻理解js事件循環機制(瀏覽器篇) 這邊文章中有個特別形象的動畫,你們能夠看着理解一下。
console.log('start')
setTimeout(function() {
console.log('setTimeout')
}, 0)
Promise.resolve().then(function() {
console.log('promise1')
}).then(function() {
console.log('promise2')
})
console.log('end')
複製代碼
start
console.log('end')
,輸出 end
promise1
, promise回調函數默認返回 undefined, promise狀態變成 fulfilled ,觸發接下來的 then回調,繼續壓入 microtask隊列,此時產生了新的微任務,會接着把當前的微任務隊列執行完,此時執行第二個 promise.then回調,輸出 promise2
setTimeout
最後的執行結果以下
表現出的狀態與瀏覽器大體相同。不一樣的是 node 中有一套本身的模型。node 中事件循環的實現依賴 libuv 引擎。Node的事件循環存在幾個階段。
若是是node10及其以前版本,microtask會在事件循環的各個階段之間執行,也就是一個階段執行完畢,就會去執行 microtask隊列中的任務。
node版本更新到11以後,Event Loop運行原理髮生了變化,一旦執行一個階段裏的一個宏任務(setTimeout,setInterval和setImmediate)就馬上執行微任務隊列,跟瀏覽器趨於一致。下面例子中的代碼是按照最新的去進行分析的。
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<──connections─── │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
複製代碼
node中事件循環的順序
外部輸入數據 --> 輪詢階段(poll) --> 檢查階段(check) --> 關閉事件回調階段(close callback) --> 定時器檢查階段(timer) --> I/O 事件回調階段(I/O callbacks) --> 閒置階段(idle, prepare) --> 輪詢階段...
這些階段大體的功能以下:
poll: 這個階段是輪詢時間,用於等待還未返回的 I/O 事件,好比服務器的迴應、用戶移動鼠標等等。 這個階段的時間會比較長。若是沒有其餘異步任務要處理(好比到期的定時器),會一直停留在這個階段,等待 I/O 請求返回結果。 check: 該階段執行setImmediate()的回調函數。
close: 該階段執行關閉請求的回調函數,好比socket.on('close', ...)。
timer階段: 這個是定時器階段,處理setTimeout()和setInterval()的回調函數。進入這個階段後,主線程會檢查一下當前時間,是否知足定時器的條件。若是知足就執行回調函數,不然就離開這個階段。
I/O callback階段: 除了如下的回調函數,其餘都在這個階段執行:
宏任務:
微任務:
Promise.nextTick process.nextTick 是一個獨立於 eventLoop 的任務隊列。 在每個 eventLoop 階段完成後會去檢查 nextTick 隊列,若是裏面有任務,會讓這部分任務優先於微任務執行。 是全部異步任務中最快執行的。
setTimeout: setTimeout()方法是定義一個回調,而且但願這個回調在咱們所指定的時間間隔後第一時間去執行。
setImmediate: setImmediate()方法從意義上將是馬上執行的意思,可是實際上它倒是在一個固定的階段纔會執行回調,即poll階段以後。
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
複製代碼
先執行宏任務(當前代碼塊也算是宏任務),而後執行當前宏任務產生的微任務,而後接着執行宏任務
script start
async1 start
, 而後執行 async2(), 輸出 async2
,把 async2() 後面的代碼 console.log('async1 end')
放到微任務隊列中promise1
,把 .then()放到微任務隊列中;注意Promise自己是同步的當即執行函數,.then是異步執行函數script end
。同步代碼(同時也是宏任務)執行完成,接下來開始執行剛纔放到微任務中的代碼async1 end
、 promise2
, 微任務中的代碼執行完成後,開始執行宏任務中的代碼,輸出 setTimeout
最後的執行結果以下
console.log('start');
setTimeout(() => {
console.log('children2');
Promise.resolve().then(() => {
console.log('children3');
})
}, 0);
new Promise(function(resolve, reject) {
console.log('children4');
setTimeout(function() {
console.log('children5');
resolve('children6')
}, 0)
}).then((res) => {
console.log('children7');
setTimeout(() => {
console.log(res);
}, 0)
})
複製代碼
這道題跟上面題目不一樣之處在於,執行代碼會產生不少個宏任務,每一個宏任務中又會產生微任務
start
children4
, 遇到setTimeout,先把 setTimeout 的代碼放到宏任務隊列②中,此時.then並不會被放到微任務隊列中,由於 resolve是放到 setTimeout中執行的children2
,此時,會把 Promise.resolve().then
放到微任務隊列中。children3
;而後開始執行宏任務②,即第二個 setTimeout,輸出 children5
,此時將.then放到微任務隊列中。children7
,遇到 setTimeout,放到宏任務隊列中。此時微任務執行完成,開始執行宏任務,輸出 children6
;最後的執行結果以下
const p = function() {
return new Promise((resolve, reject) => {
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 0)
resolve(2)
})
p1.then((res) => {
console.log(res);
})
console.log(3);
resolve(4);
})
}
p().then((res) => {
console.log(res);
})
console.log('end');
複製代碼
p1.then
會先放到微任務隊列中,接着往下執行,輸出 3
p().then
會先放到微任務隊列中,接着往下執行,輸出 end
p1.then
,輸出 2
, 接着執行p().then
, 輸出 4
resolve(1)
,可是此時 p1.then
已經執行完成,此時 1
不會輸出。最後的執行結果以下
你能夠將上述代碼中的 resolve(2)
註釋掉, 此時 1纔會輸出,輸出結果爲 3 end 4 1
。
const p = function() {
return new Promise((resolve, reject) => {
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 0)
})
p1.then((res) => {
console.log(res);
})
console.log(3);
resolve(4);
})
}
p().then((res) => {
console.log(res);
})
console.log('end');
複製代碼
最後強烈推薦幾個很是好的講解 event loop 的視頻: