"Event Loop是一個程序結構,用於等待和發送消息和事件。 (a programming construct that waits for and dispatches events or messages in a program.)"
複製代碼
舉一個你們都熟知的栗子, 這樣更能客觀的理解。node
你們都知道深夜食堂吧。廚師就一我的(服務端 Server)。最多再來一個服務生( 調度員 Event Loop )。晚上吃飯的客人(客戶端 Client)不少。數據庫
1. 客人向服務生點完菜,就幹本身事情,不用一直等着服務生, 服務生把一我的點的菜單送到廚師,
又去服務新的客人...
2. 廚師(服務端)只負責作客人們點的菜。
3. 服務生(調度員)不停的看廚師,一旦廚師作好菜了,按照標號送到相應的 客人(客戶端)座位上
複製代碼
假設咱們把 廚師 和 服務生 都比做 服務端的線程的話, 服務生線程 爲
主線程
, 廚師線程 爲
消息線程
。 客人每點一個菜。服務生就向廚師發出一個消息。並保留該消息的「標識」(
回調函數
)用來接收廚師炒好的菜,並把菜送到相應的客人手中。
無論店裏的客人多少,也無論每一份菜須要多久的時間作好。就只有廚師這一我的忙活。廚師一次只能服務一個客人。那這樣的服務模式效率就比較低了。中途等待的時間比較長。 筆者認爲 同步模式 就是沒有 「服務生線程」, 廚師線程升級爲 主線程
。api
1. 第一個客人點了一份 "讀取文件" , 炒好一份 "讀取文件" 須要花費 1 分鐘
2. 必須等第一個客人的菜炒好後,第二個客人才能點,而且點了一份 "讀取數據庫",
炒好一份 "讀取數據庫" 須要花費 2 分鐘
3. 第三個客人點了一份 ...
複製代碼
從圖中能夠看出紅色部份都是等待時間(或者是阻塞時間), 至關浪費資源。promise
假設咱們如今只知道一種代碼的執行方式 "同步執行", 也就是代碼從上到下 按順序執行。若是遇到 setTimeout , 也先這樣理解。(實際上setTimeout 自己是當即執行的,只是回調函數異步執行)瀏覽器
console.log(1); //執行順序1
setTimeout(function(){}, 1000); //執行順序2
console.log(2); //執行順序3
複製代碼
圖表更能直觀的反應這個概念: bash
主線程
不停的接收請求 request 和 響應請求 response, 真正處理任務的被 消息線程 event loop
安排其餘相應的程序去執行,並接收相應的相應程序返回的消息。而後 reponse 給客戶端。多線程
1. 主線程乾的事情很是簡單,即 接收請求,響應請求, 所以能夠可以處理更多的請求。而不用等待。
2. 消息線程維護請求,並把真正要作的事情交給對應的程序,並接收對應程序的回調消息,返回給 主線程
複製代碼
你跟你的女神表白,你女神當即回覆你,而你也一直再等女神的回覆異步
你跟你的女神表白, 你表白後,沒有等女神來得及回覆,你去忙你本身的事情了。你的女神當即回覆了你socket
你跟你的女神表白, 你女神沒有當即回覆你,說要考慮考慮,過幾天答覆你,而你也一直再等女神的回覆函數
你跟你的女神表白,你表白後, 沒有等女神的回覆。你去忙你本身的事情了,女神也說她要考慮考慮,過幾天再回復你
阻塞非阻塞 是指調用者
(表白的那我的) 同步異步 是指被調用者
(被表白的那我的)
宏任務 setTimeout , setInterval, setImmediate, I / O 操做
微任務 process.nextTick , 原生Promise (有些實現的Promise將then方法放到了宏任務中), Mutation Observer
console.log(1);
Promise.resolve('123').then(()=>{console.log('then')})
process.nextTick(function () {
console.log('nextTick')
})
console.log(2);
複製代碼
process.nextTick 優先於 promise.then 方法執行
function one() {
let a = 1;
two();
function two() {
console.log(a);
let b = 2;
function three() {
//debugger;
console.log(b);
}
three();
}
}
one();
複製代碼
毫無疑問的是,上面這段代碼執行的結果爲:
1
2
複製代碼
在棧中都是以同步任務的方式存在:
再來看下面這段代碼:
console.log(1);
setTimeout(function(){
console.log(2);
})
console.log(3);
複製代碼
執行結果爲:
1
3
2
複製代碼
那究竟是怎樣執行的呢?
順便提一句:文章最開始就說 setTimeout函數自己的執行時機和其回調函數執行的時機是不同的。//宏任務
setTimeout(function(){
console.log(2);
})
//微任務
let p = new Promise((resolve, reject) => {
resolve(3);
});
p.then((data) => {
console.log(data);
}, (err)=>{
})
console.log(4);
複製代碼
執行結果爲:
1
4
3
2
複製代碼
從這個能夠看到。微任務消息隊列的執行的
優先
於宏任務的消息隊列.
console.log(1);
//宏任務
setTimeout(function(){
console.log(2);
})
//微任務
let p = new Promise((resolve, reject) => {
resolve(4);
});
p.then((data) => {
console.log(data);
}, (err)=>{
})
setTimeout(function(){
console.log(3);
})
console.log(5);
複製代碼
執行結果爲:
1
5
4
2
3
複製代碼
每一次事件循環機制過程當中,會將當前宏任務 或者 微任務消息隊列中的任務都執行完成。而後再以前其餘隊列。
當前
的全部任務(同步任務)都已經執行完成。咱們先來看看node是怎樣運行的:
Event Loop 在整個Node 運行機制中佔據着舉足輕重的地位。是其核心。
(Event Loop 不一樣階段)每一個階段都有一個執行回調的FIFO隊列。 雖然每一個階段都有其特定的方式,但一般狀況下,當事件循環進入給定階段時,它將執行特定於該階段的任何操做, 而後在該階段的隊列中執行回調,直到隊列耗盡或回調的最大數量 已執行。 當隊列耗盡或達到回調限制時,事件循環將移至下一個階段,依此類推。
timers:此階段執行由setTimeout()和setInterval()調度的回調。
pending callbacks:執行I / O回調,推遲到下一個循環迭代。
idle,prepare:只在內部使用。
poll:檢索新的I / O事件; 執行I / O相關的回調函數; 適當時節點將在此處阻塞。
check:setImmediate()回調在這裏被調用。
close backbacks:一些關閉回調,例如 socket.on('close',...)。
複製代碼
timers階段
須要注意的是:
const fs = require('fs');
function someAsyncOperation(callback) {
//假設須要95ms須要執行完成
fs.readFile('/path/to/file', callback);
}
const timeoutScheduled = Date.now();
//定義100ms後執行
setTimeout(() => {
const delay = Date.now() - timeoutScheduled;
console.log(`${delay}ms have passed since I was scheduled`);
}, 100);
// 執行someAsyncOperation須要消耗95ms執行
someAsyncOperation(() => {
const startCallback = Date.now();
// do something that will take 10ms...
while (Date.now() - startCallback < 10) {
// do nothing
}
});
複製代碼
分析上述代碼:
pending callbacks階段
此階段爲某些系統操做(如TCP錯誤類型)執行回調。例如,
若是嘗試鏈接時TCP套接字收到ECONNREFUSED,則某些* nix系統要等待報告錯誤。這將排隊等候在待處理的回調階段執行。
複製代碼
poll階段
1.計算應該阻塞和輪詢I / O的時間
2.處理輪詢隊列中的事件。
複製代碼
當事件循環進入poll階段而且沒有計時器時,會發生如下兩件事之一:
1. 若是輪詢隊列不爲空,則事件循環將遍歷其回調隊列,同步執行它們,直到隊列耗盡或達到系統相關硬限制。
2. 若是輪詢隊列爲空,則會發生如下兩件事之一:
2.1 若是腳本已經過setImmediate()進行調度,則事件循環將結束輪詢階段並繼續執行(check階段)檢查階段以執行這些預約腳本。
2.2 若是腳本沒有經過setImmediate()進行調度,則事件循環將等待將回調添加到隊列中,而後當即執行它們。
複製代碼
一旦輪詢隊列爲空,事件循環將檢查已達到時間閾值的定時器。若是一個或多個定時器準備就緒,則事件循環將回退到定時器階段以執行這些定時器的回調。
// poll的下一個階段時check
// 有check階段就會走到check中
let fs = require('fs');
fs.readFile('./1.txt',function () { //輪詢隊列已經執行完成,爲空,即2.1中描述的
setTimeout(() => {
console.log('setTimeout')
}, 0);
setImmediate(() => {
console.log('setImmediate')
});
});
複製代碼
上面這段代碼執行的過程階段爲:
check階段
setImmediate()其實是一個特殊的定時器,它在事件循環的一個單獨的階段中運行。它使用libuv API來調度回調,以在輪詢(poll)階段完成後執行。
close callback階段
若是套接字socks或句柄忽然關閉(例如socket.destroy()),則在此階段將發出'close'事件。 不然它將經過process.nextTick()觸發事件。
複製代碼
setImmediate()用於在當前輪詢階段完成後執行腳本。
setTimeout()計劃腳本在通過最小閾值(以毫秒爲單位)後運行。
複製代碼
定時器執行的順序取決於它們被調用的上下文。 若是二者都是在主模塊內調用的,那麼時序將受到進程性能的限制(可能會受到計算機上運行的其餘應用程序的影響)。
簡言之: setTimediate 和 setTimeout 的執行順序不肯定。
// setTimeout和setImmediate順序是不固定,看node準備時間
setTimeout(function () {
console.log('setTimeout')
},0);
setImmediate(function () {
console.log('setImmediate')
});
複製代碼
輸出的結果多是這樣
setTimeout
setImmediate
複製代碼
也有多是這樣
setImmediate
setTimeout
複製代碼
But, 若是在I / O週期內移動這兩個調用,則當即回調老是首先執行, 能夠爬樓參考 poll階段的介紹。
使用setImmediate()的主要優勢是,若是在I / O週期內進行調度,將始終在任何計時器以前執行setImmediate(),而無論有多少個計時器。
爲何要用process.nextTick
容許用戶處理錯誤,清理任何不須要的資源,或者可能在事件循環繼續以前再次嘗試請求。 有時須要在調用堆棧解除以後但事件循環繼續以前容許回調運行。
process.nextTick()沒有顯示在圖中,即便它是異步API的一部分。 這是由於process.nextTick()在技術上並非事件循環的一部分。 相反,nextTickQueue將在當前操做完成後處理,而無論事件循環的當前階段如何。
回顧一下事件循環機制,只要你在給定的階段調用process.nextTick(),全部傳遞給process.nextTick()的回調都將在事件循環繼續以前被解析。
// nextTick是隊列切換時執行的,timer->check隊列 timer1->timer2不叫且
setImmediate(() => {
console.log('setImmediate1')
setTimeout(() => {
console.log('setTimeout1')
}, 0);
})
setTimeout(()=>{
process.nextTick(()=>console.log('nextTick'))
console.log('setTimeout2')
setImmediate(()=>{
console.log('setImmediate2')
})
},0);
複製代碼
在討論事件循環(Event Loop)的時候,要時刻知道 宏任務,微任務,process.nextTick等概念。 上面代碼執行的結果可能爲:
setTimeout2
nextTick
setImmediate1
setImmediate2
setTimeout1
複製代碼
或者
setImmediate1
setTimeout2
setTimeout1
nextTick
setImmediate2
複製代碼
爲何呢? 這個就留給各位看官的一個思考題吧。歡迎留言討論~