相關係列: 從零開始的前端築基之旅(面試必備,持續更新~)javascript
javascript是一門單線程語言,在最新的HTML5中提出了Web-Worker,但javascript是單線程這一核心仍未改變,無論誰寫的代碼,都得一句一句的來執行。html
當咱們打開網站時,網頁的渲染過程包括了一大堆任務,好比頁面元素的渲染。script腳本的執行,經過網絡請求加載圖片音樂之類。若是一個一個的順序執行,趕上任務耗時過長,就會發生卡頓現象。因而,事件循環(Event Loop)應運而生。前端
事件循環,能夠理解爲實現異步的一種方式。event loop在HTML Standard中的定義:java
爲了協調事件,用戶交互,腳本,渲染,網絡等,用戶代理必須使用本節所述的
event loop
。node
JavaScript
有一個主線程 main thread
,和調用棧 call-stack
也稱之爲執行棧。全部的任務都會放到調用棧中等待主線程來執行。待執行的任務就是流水線上的原料,只有前一個加工完,後一個才能進行。event loops就是把原料放上流水線的工人,協調用戶交互,腳本,渲染,網絡這些不一樣的任務。git
將待執行任務分爲兩類:github
主線程自上而下執行全部代碼web
Event Table
並註冊相對應的回調函數Event Table
會將這個函數移入 Event Queue
Event Queue
中讀取任務,進入到主線程去執行。一個event loop有一個或者多個task隊列。當用戶代理安排一個任務,必須將該任務增長到相應的event loop的一個tsak隊列中。面試
task也被稱爲macrotask(宏任務),是一個先進先出的隊列,由指定的任務源去提供任務。編程
task任務源很是寬泛,總結來講task任務源包括:
因此 Task Queue
就是承載任務的隊列。而 JavaScript
的 Event Loop
就是會不斷地過來找這個 queue
,問有沒有 task
能夠運行運行。
每個event loop都有一個microtask隊列,一個microtask會被排進microtask隊列而不是task隊列。
microtask 隊列和task 隊列有些類似,都是先進先出的隊列,由指定的任務源去提供任務,不一樣的是一個 event loop裏只有一個microtask 隊列。
一般認爲是microtask任務源有:
在Promises/A+規範的Notes 3.1中說起了promise的then方法能夠採用「宏任務(macro-task)」機制或者「微任務(micro-task)」機制來實現。因此不一樣瀏覽器對promise的實現可能存在差別。
事件循環的順序,決定js代碼的執行順序。進入總體代碼(宏任務)後,開始第一次循環。接着執行全部的微任務。而後再次從宏任務開始,找到其中一個任務隊列執行完畢,再執行全部的微任務。
執行完
microtask
隊列裏的任務,有可能會渲染更新。(瀏覽器很聰明,在一幀之內的屢次dom變更瀏覽器不會當即響應,而是會積攢變更以最高60HZ的頻率更新視圖)
以下面代碼,setTimeout
就是一個異步任務,
console.log('start')
setTimeout(()=>{
console.log('setTimeout')
});
console.log('end');
複製代碼
console.log('start');
setTimeout
發現是一個異步任務,就先註冊了一個異步的回調console.log('end')
Event Queue
是否有待執行的 task,只要主線程的task queue
沒有任務執行了,主線程就一直在這等着console.log('setTimeout')
。js引擎存在monitoring process進程,會持續不斷的檢查主線程執行棧是否爲空,一旦爲空,就會去Event Queue那裏檢查是否有等待被調用的函數。
注意,只有等主線程執行完畢,纔會檢查Event Queue
是否有待執行的 task,所以可能會出現另外一種狀況。
console.log('start')
setTimeout(()=>{
console.log('setTimeout')
}, 3000);
todo(); // 假定這裏是一個耗時10秒的操做
複製代碼
正常狀況下,控制檯輸出應該是這樣的
start
// 等待3秒
setTimeout
複製代碼
而實際上,輸出大概是這樣的:
start
// 等待10秒
setTimeout
複製代碼
從新分析一下執行流程:
console.log('start');
setTimeout
發現是一個異步任務,就先註冊了一個異步的回調todo()
timeout
完成,打印任務進入Event QueueEvent Queue
是否有待執行的 task,console.log('setTimeout')
。setTimeout
這個函數,是通過指定時間後,把要執行的任務加入到Event Queue中,與上一個栗子不一樣,當計時事件完成後,主線程任務並無執行完畢。只有等主線程執行完本輪代碼後,纔會查詢Event Queue。
因此,等待大約10秒後控制檯纔有第二次輸出。
setTimeout(fn,0)
setTimeout(fn,0)
的含義是,指定某個任務在主線程最先可得的空閒時間執行,意思就是隻要主線程執行棧內的同步任務所有執行完成,棧爲空就立刻執行。
即使主線程爲空,0毫秒實際上也是達不到的。根據HTML的標準,最低是4毫秒。
setInterval
會每隔指定的時間將註冊的函數置入Event Queue,若是前面的任務耗時過久,那麼一樣須要等待。
與setTimeout
類似,對於setInterval(fn,ms)
來講,不是每過ms
秒會執行一次fn
,而是每過ms
秒,會有fn
進入Event Queue。一旦**setInterval
的回調函數fn
執行時間因爲主線程繁忙超過了延遲時間ms
,那麼就徹底看不出來有時間間隔,而是會連續執行。**
process.nextTick(callback)
相似node.js版的"setTimeout",在事件循環的下一次循環中調用 callback 回調函數。
以一段代碼爲例:
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})
console.log('console');
複製代碼
主線程自上而下執行全部代碼
setTimeout
,那麼將其回調函數註冊後分發到宏任務Event Queue。Promise
,new Promise
當即執行,then
函數分發到微任務Event Queue。console.log()
,當即執行。then
在微任務Event Queue裏面,執行。setTimeout
對應的回調函數,當即執行。執行完一個宏任務後,會執行全部的微任務,而後再執行一個宏任務
console.log('start');
Promise.resolve()
.then(function promise1() { // then1
console.log('promise1');
})
.then(function () { // then2
console.log('promise2')
})
setTimeout(function setTimeout1() { // setTimeout1
console.log('setTimeout1')
Promise.resolve().then(function promise2() { // then3
console.log('promise3');
})
}, 0)
setTimeout(function setTimeout2() { // setTimeout1
console.log('setTimeout2')
}, 0)
console.log('end')
複製代碼
分析下執行流程:
console.log()
,當即執行, 輸出 start。Promise
,then
被分發到微任務Event Queue中。咱們記爲then1
。setTimeout
,其回調函數被分發到宏任務Event Queue中。咱們暫且記爲setTimeout1
。setTimeout
,其回調函數被分發到宏任務Event Queue中,咱們記爲setTimeout2
。console.log()
,當即執行, 輸出 end。第一輪執行結束,控制檯輸出 stert,end,此時,任務隊列以下:
宏任務Event Queue | 微任務Event Queue |
---|---|
setTimeout1 | then1 |
setTimeout2 |
執行微任務:
微任務執行完畢,第二輪循環開始,轉入宏任務 setTimeout1:
console.log()
,當即執行, 輸出 setTimeout1。Promise
,then
被分發到微任務Event Queue中。咱們記爲then3
。執行微任務 then3:
console.log()
,當即執行, 輸出 promise3。第三輪循環開始,執行宏任務setTimeout2
console.log()
,當即執行, 輸出 setTimeout2最後,控制檯輸出結果爲
stert
end
promise1
promise2
setTimeout1
promise3
setTimeout2
複製代碼
Node中的Event Loop
是基於libuv
實現的,而libuv
是 Node 的新跨平臺抽象層,libuv
使用異步,事件驅動的編程方式,核心是提供i/o
的事件循環和異步回調。libuv
的API
包含有時間,非阻塞的網絡,異步文件操做,子進程等等。
Node 的 Event Loop 分爲 6 個階段:
setTimeout()
和 setInterval()
中到期的callback。I/O
callback會被延遲到這一輪的這一階段執行I/O
callback,在適當的條件下會阻塞在這個階段setImmediate
的callbackclose
事件的callback,例如socket.on('close'[,fn])
、http.server.on('close, fn)
上面六個階段都不包括 process.nextTick()
timers 階段會執行 setTimeout
和 setInterval
回調,而且是由 poll 階段控制的。
在 timers 階段其實使用一個最小堆而不是隊列來保存全部的元素,由於timeout的callback是按照超時時間的順序來調用的,並非先進先出的隊列邏輯)。而爲何 timer 階段在第一個執行階梯上其實也不難理解。在 Node 中定時器指定的時間也是不許確的,而這樣,就能儘量的準確了,讓其回調函數儘快執行。
pending callbacks 階段實際上是 I/O
的 callbacks 階段。好比一些 TCP 的 error 回調等。
poll 階段主要有兩個功能:
I/O
回調當時Event Loop 進入到 poll 階段而且 timers 階段沒有任何可執行的 task 的時候(也就是沒有定時器回調),將會有如下兩種狀況
check 階段在 poll 階段以後,setImmediate()
的回調會被加入check隊列中,他是一個使用libuv API
的特殊的計數器。
一般在代碼執行的時候,Event Loop 最終會到達 poll 階段,而後等待傳入的連接或者請求等,可是若是已經指定了setImmediate()而且這時候 poll 階段已經空閒的時候,則 poll 階段將會被停止而後開始 check 階段的執行。
若是一個 socket 或者事件處理函數忽然關閉/中斷(好比:socket.destroy()
),則這個階段就會發生 close
的回調執行。
setImmediate
在 poll 階段後執行,即check 階段setTimeout
在 poll 空閒時且設定時間到達的時候執行,在 timer 階段計時器的執行順序將根據調用它們的上下文而有所不一樣。 若是二者都是從主模塊中調用的,則時序將受到進程性能的限制。
若是不在I / O
週期(即主模塊)內,則兩個計時器的執行順序是不肯定的,由於它受進程性能的約束:
// timeout_vs_immediate.js
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
複製代碼
$ node timeout_vs_immediate.js
timeout
immediate
$ node timeout_vs_immediate.js
immediate
timeout
複製代碼
若是在一個I/O
週期內移動這兩個調用,則始終首先執行當即回調:
// timeout_vs_immediate.js
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
複製代碼
與setTimeout()
相比,使用setImmediate()
的主要優勢是,若是在I / O
週期內安排了任何計時器,則setImmediate()
將始終在任何計時器以前執行,而與存在多少計時器無關。
process.nextTick()
從技術上講不是Event Loop的一部分。 相反,不管當前事件循環的當前階段如何,若是存在 nextTickQueue
,都將在當前操做完成以後處理nextTickQueue,
優先於其餘 microtask
。
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
})
})
})
})
// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1
複製代碼
瀏覽器環境下,microtask的任務隊列是每一個macrotask執行完以後執行。而在Node.js中,microtask會在事件循環的各個階段之間執行,也就是一個階段執行完畢,就會去執行microtask隊列的任務。
若是你收穫了新知識,請給做者點個贊吧,左側邊欄第一個按鈕,用力的點一下~
參考文章: