The last time, I have learnedhtml
【THE LAST TIME】一直是我想寫的一個系列,旨在厚積薄發,重溫前端。前端
也是給本身的查缺補漏和技術分享。html5
歡迎你們多多評論指點吐槽。node
系列文章均首發於公衆號【全棧前端精選】,筆者文章集合詳見Nealyang/personalBlog。目錄皆爲暫定git
首先咱們須要聲明下,JavaScript
的執行和運行是兩個不一樣概念的,執行,通常依賴於環境,好比 node
、瀏覽器、Ringo
等, JavaScript 在不一樣環境下的執行機制可能並不相同。而今天咱們要討論的 Event Loop
就是 JavaScript
的一種執行方式。因此下文咱們還會梳理 node
的執行方式。而運行呢,是指JavaScript 的解析引擎。這是統一的。github
此篇文章中,這個小標題下,咱們只須要牢記一句話: JavaScript 是單線程語言 ,不管HTML5
裏面 Web-Worker
仍是 node 裏面的cluster
都是「紙老虎」,並且 cluster
仍是進程管理相關。這裏讀者注意區分:進程和線程。面試
既然 JavaScript
是單線程語言,那麼就會存在一個問題,全部的代碼都得一句一句的來執行。就像咱們在食堂排隊打飯,必須一個一個排隊點菜結帳。那些沒有排到的,就得等着~ajax
在詳解執行機制以前,先梳理一下 JavaScript
的一些基本概念,方便後面咱們說到的時候大夥兒內心有個印象和大概的輪廓。chrome
什麼是 Event Loop?編程
其實這個概念仍是比較模糊的,由於他必須得結合着運行機制來解釋。
JavaScript
有一個主線程 main thread
,和調用棧 call-stack
也稱之爲執行棧。全部的任務都會放到調用棧中等待主線程來執行。
暫且,咱們先理解爲上圖的大圈圈就是 Event Loop 吧!而且,這個圈圈,一直在轉圈圈~ 也就是說,JavaScript
的 Event Loop
是伴隨着整個源碼文件生命週期的,只要當前 JavaScript
在運行中,內部的這個循環就會不斷地循環下去,去尋找 queue
裏面能執行的 task
。
task
,就是任務的意思,咱們這裏理解爲每個語句就是一個任務
console.log(1);
console.log(2);
複製代碼
如上語句,其實就是就能夠理解爲兩個 task
。
而 queue
呢,就是FIFO
的隊列!
因此 Task Queue
就是承載任務的隊列。而 JavaScript
的 Event Loop
就是會不斷地過來找這個 queue
,問有沒有 task
能夠運行運行。
同步任務說白了就是主線程來執行的時候當即就能執行的代碼,好比:
console.log('this is THE LAST TIME');
console.log('Nealyang');
複製代碼
代碼在執行到上述 console
的時候,就會當即在控制檯上打印相應結果。
而所謂的異步任務就是主線程執行到這個 task
的時候,「唉!你等會,我如今先不執行,等我 xxx 完了之後我再來等你執行」 注意上述我說的是等你來執行。
說白了,異步任務就是你先去執行別的 task,等我這 xxx 完以後再往 Task Queue 裏面塞一個 task 的同步任務來等待被執行
setTimeout(()=>{
console.log(2)
});
console.log(1);
複製代碼
如上述代碼,setTimeout
就是一個異步任務,主線程去執行的時候遇到 setTimeout
發現是一個異步任務,就先註冊了一個異步的回調,而後接着執行下面的語句console.log(1)
,等上面的異步任務等待的時間到了之後,在執行console.log(2)
。具體的執行機制會在後面剖析。
Event Table
並註冊相對應的回調函數Event Table
會將這個函數移入 Event Queue
Event Queue
中讀取任務,進入到主線程去執行。上述動做不斷循環,就是咱們所說的事件循環(Event Loop
)。
ajax({
url:www.Nealyang.com,
data:prams,
success:() => {
console.log('請求成功!');
},
error:()=>{
console.log('請求失敗~');
}
})
console.log('這是一個同步任務');
複製代碼
Event Table
,分別註冊了onError
和onSuccess
回調函數。console.log('這是一個同步任務');
Event Queue
是否有待執行的 task,這裏是不斷地檢查,只要主線程的task queue
沒有任務執行了,主線程就一直在這等着push
到Event Queue
。(步驟 三、4 沒有前後順序而言)Event Queue
裏有 task
能夠執行了,執行對應的回調任務。JavaScript
的任務不只僅分爲同步任務和異步任務,同時從另外一個維度,也分爲了宏任務(MacroTask
)和微任務(MicroTask
)。
先說說 MacroTask
,全部的同步任務代碼都是MacroTask
(這麼說其實不是很嚴謹,下面解釋),setTimeout
、setInterval
、I/O
、UI Rendering
等都是宏任務。
MicroTask
,爲何說上述不嚴謹我卻仍是強調全部的同步任務都是 MacroTask
呢,由於咱們僅僅須要記住幾個 MicroTask
便可,排除法!別的都是 MacroTask
。MicroTask
包括:Process.nextTick
、Promise.then catch finally
(注意我不是說 Promise)、MutationObserver
。
當咱們梳理完哪些是 MicroTask
,除了那些別的都是 MacroTask
後,哪些是同步任務,哪些又是異步任務後,這裏就應該完全的梳理下JavaScript 的執行機制了。
如開篇說到的,執行和運行是不一樣的,執行要區分環境。因此這裏咱們將 Event Loop
的介紹分爲瀏覽器和 Node 兩個環境下。
先放圖鎮樓!若是你已經理解了這張圖的意思,那麼恭喜你,你徹底能夠直接閱讀 Node 環境下的 Event Loop
章節了!
setTimeout
就是等多長時間來執行這個回調函數。setInterval
就是每隔多長時間來執行這個回調。
let startTime = new Date().getTime();
setTimeout(()=>{
console.log(new Date().getTime()-startTime);
},1000);
複製代碼
如上代碼,顧名思義,就是等 1s 後再去執行 console
。放到瀏覽器下去執行,OK,如你所願就是如此。
可是此次咱們在探討 JavaScript 的執行機制,因此這裏咱們得探討下以下代碼:
let startTime = new Date().getTime();
console.log({startTime})
setTimeout(()=>{
console.log(`開始執行回調的相隔時差:${new Date().getTime()-startTime}`);
},1000);
for(let i = 0;i<40000;i++){
console.log(1)
}
複製代碼
如上運行,setTimeout
的回調函數等到 4.7s 之後才執行!而這時候,咱們把 setTimeout
的 1s 延遲給刪了:
let startTime = new Date().getTime();
console.log({startTime})
setTimeout(()=>{
console.log(`開始執行回調的相隔時差:${new Date().getTime()-startTime}`);
},0);
for(let i = 0;i<40000;i++){
console.log(1)
}
複製代碼
結果依然是等到 4.7s 後才執行setTimeout 的回調。貌似 setTimeout 後面的延遲並無產生任何效果!
其實這麼說,又應該回到上面的那張 JavaScript 執行的流程圖了。
setTimeout
這裏就是簡單的異步,咱們經過上面的圖來分析上述代碼的一步一步執行狀況
JavaScript
自上而下執行代碼console.log({startTime})
分別做爲一個 task
,壓入到當即執行棧中被執行。setTImeout
是一個異步任務,則註冊相應回調函數。(異步函數告訴你,js 你先別急,等 1s 後我再將回調函數:console.log(xxx)
放到 Task Queue
中)Task Queue
中了。Task Queue
中的任務,發現異步的回調 task 已經在裏面了,因此接着執行。其實上述的不只僅是 timeout,而是任何異步,好比網絡請求等。
就比如,我六點鐘下班了,能夠安排下本身的活動了!
而後收拾電腦(同步任務)、收拾書包(同步任務)、給女友打電話說出來吃飯吧(必然是異步任務),而後女友說你等會,我先化個妝,等我畫好了call你。
那我不能幹等着呀,就接着作別的事情,好比那我就在改個 bug 吧,你好了通知我。結果等她一個小時後說我化好妝了,咱們出去吃飯吧。不行!我 bug 尚未解決掉呢?你等會。。。。其實這個時候你的一小時化妝仍是 5 分鐘化妝都已經毫無心義了。。。由於哥哥這會沒空~~
若是我 bug 在半個小時就解決完了,沒別的任務須要執行了,那麼就在這等着呀!必須等着!隨時待命!。而後女友來電話了,我化完妝了,咱們出去吃飯吧,那麼恰好,咱們在你的完成了請求或者 timeout 時間到了後我恰好閒着,那麼我必須當即執行了。
說完了 setTimeout
,固然不能錯過他的孿生兄弟:setInterval
。對於執行順序來講,setInterval
會每隔指定的時間將註冊的函數置入 Task Queue
,若是前面的任務耗時過久,那麼一樣須要等待。
這裏須要說的是,對於 setInterval(fn,ms)
來講,咱們制定沒 xx ms
執行一次 fn
,實際上是沒 xx ms
,會有一個fn
進入到 Task Queue
中。一旦 setInterval 的回調函數fn
執行時間超過了xx ms,那麼就徹底看不出來有時間間隔了。 仔細回味回味,是否是那麼回事?
關於 Promise
的用法,這裏就不過過多介紹了,後面會在寫《【THE LAST TIME】完全吃透 JavaScript 異步》 一文的時候詳細介紹。這裏咱們只說 JavaScript 的執行機制。
如上所說,promise.then
、catch
和 finally
是屬於 MicroTask
。這裏主要是異步的區分。展開說明以前,咱們結合上述說的,再來「扭曲」梳理一下。
爲了不初學者這時候腦子有點混亂,咱們暫時忘掉 JavaScript 異步任務! 咱們暫且稱之爲待會再執行的同步任務。
有了如上約束後,咱們能夠說,JavaScript 從一開始就自上而下的執行每個語句(Task
),這時候只能遇到立馬就要執行的任務和待會再執行的任務。對於那待會再執行的任務等到能執行了,也不會當即執行,你得等js 執行完這一趟才行
就像作公交車同樣,公交車不等人呀,公交車路線上有人就會停(農村公交!麼得站牌),可是等公交車來,你跟司機說,我肚子疼要拉x~這時候公交不會等你。你只能拉完之後等公交下一趟再來(大山裏!一個路線就一趟車)。
OK!你拉完了。。。等公交,公交也很快到了!可是,你不能立立刻車,由於這時候前面有個孕婦!有個老人!還有熊孩子,你必須得讓他們先上車,而後你才能上車!
而這些 孕婦、老人、熊孩子所組成的就是傳說中的 MicroTask Queue
,並且,就在你和你的同事、朋友就必須在他們後面上車。
這裏咱們沒有異步的概念,只有一樣的一次循環回來,有了兩種隊伍,一種優先上車的隊伍叫作MicroTask Queue
,而你和你的同事這幫壯漢組成的隊伍就是宏隊伍(MacroTask Queue
)。
一句話理解:一次事件循環回來後,開始去執行 Task Queue
中的 task
,可是這裏的 task
有優先級。因此優先執行 MicroTask Queue
中的 task ,執行完後在執行MacroTask Queue
中的 task
理論都扯完了,也不知道你懂沒懂。來,期中考試了!
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
複製代碼
不必搞個 setTimeout 有加個 Promise,Promise 裏面再整個 setTimeout 的例子。由於只要上面代碼你懂了,無非就是公交再來一趟而已!
若是說了這麼多,仍是沒能理解上圖,那麼公衆號內回覆【1】,手摸手指導!
Node中的Event Loop
是基於libuv
實現的,而libuv
是 Node 的新跨平臺抽象層,libuv
使用異步,事件驅動的編程方式,核心是提供i/o
的事件循環和異步回調。libuv
的API
包含有時間,非阻塞的網絡,異步文件操做,子進程等等。
Event Loop就是在libuv
中實現的。因此關於 Node 的 Event Loop
學習,有兩個官方途徑能夠學習:
在學習 Node 環境下的 Event Loop
以前呢,咱們首先要明確執行環境,Node 和瀏覽器的Event Loop是兩個有明確區分的事物,不能混爲一談。nodejs的event是基於libuv,而瀏覽器的event loop則在html5的規範中明肯定義。
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
複製代碼
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 中定時器指定的時間也是不許確的,而這樣,就能儘量的準確了,讓其回調函數儘快執行。
如下是官網給出的例子:
const fs = require('fs');
function someAsyncOperation(callback) {
// Assume this takes 95ms to complete
fs.readFile('/path/to/file', callback);
}
const timeoutScheduled = Date.now();
setTimeout(() => {
const delay = Date.now() - timeoutScheduled;
console.log(`${delay}ms have passed since I was scheduled`);
}, 100);
// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
const startCallback = Date.now();
// do something that will take 10ms...
while (Date.now() - startCallback < 10) {
// do nothing
}
});
複製代碼
當進入事件循環時,它有一個空隊列(fs.readFile()
還沒有完成),所以定時器將等待剩餘毫秒數,當到達95ms時,fs.readFile()
完成讀取文件而且其完成須要10毫秒的回調被添加到輪詢隊列並執行。
當回調結束時,隊列中再也不有回調,所以事件循環將看到已達到最快定時器的閾值,而後回到timers階段以執行定時器的回調。 在此示例中,您將看到正在調度的計時器與正在執行的回調之間的總延遲將爲105毫秒。
pending callbacks 階段實際上是 I/O
的 callbacks 階段。好比一些 TCP 的 error 回調等。
舉個栗子:若是TCP socket ECONNREFUSED
在嘗試connect
時receives
,則某些* nix系統但願等待報告錯誤。 這將在pending callbacks階段執行。
poll 階段主要有兩個功能:
I/O
回調當時Event Loop 進入到 poll 階段而且 timers 階段沒有任何可執行的 task 的時候(也就是沒有定時器回調),將會有如下兩種狀況
一旦 poll queue 爲空,Event Loop就回去檢查timer 階段的任務。若是有的話,則會回到 timer 階段執行回調。
check 階段在 poll 階段以後,setImmediate()
的回調會被加入check隊列中,他是一個使用libuv API
的特殊的計數器。
一般在代碼執行的時候,Event Loop 最終會到達 poll 階段,而後等待傳入的連接或者請求等,可是若是已經指定了setImmediate()而且這時候 poll 階段已經空閒的時候,則 poll 階段將會被停止而後開始 check 階段的執行。
若是一個 socket 或者事件處理函數忽然關閉/中斷(好比:socket.destroy()
),則這個階段就會發生 close
的回調執行。不然他會經過 process.nextTick()
發出。
setImmediate()
和 setTimeout()
很是的類似,區別取決於誰調用了它。
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()
並未顯示在圖中,即便它是異步API的一部分。 因此他擁有一個本身的隊列:nextTickQueue
。
這是由於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
複製代碼
從使用者角度而言,這兩個名稱很是的容易讓人感受到困惑。
process.nextTick()
在同一階段當即觸發setImmediate()
在事件循環的如下迭代或「tick」中觸發貌似這兩個名稱應該呼喚下!的確~官方也這麼認爲。可是他們說這是歷史包袱,已經不會更改了。
這裏仍是建議你們儘量使用setImmediate。由於更加的讓程序可控容易推理。
至於爲何仍是須要 process.nextTick
,存在即合理。這裏建議你們閱讀官方文檔:why-use-process-nexttick。
一句話總結其中:瀏覽器環境下,microtask的任務隊列是每一個macrotask執行完以後執行。而在Node.js中,microtask會在事件循環的各個階段之間執行,也就是一個階段執行完畢,就會去執行microtask隊列的任務。
上圖來自浪裏行舟
來~期末考試了
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
複製代碼
評論區留下你的答案吧~~老鐵!
關注公衆號: 【全棧前端精選】 每日獲取好文推薦。
公衆號內回覆 【1】,加入全棧前端學習羣,一塊兒交流。