文章資料來自
Node.js 事件循環機制
JS靈魂之問(下)
EventLoop的中國名字叫事件循環,這個玩意真的是高深莫測,一般開發都用不到,代碼只管寫就行,雖然不用懂,但是面試就是要問,這對我這種小菜雞真是滿滿的惡意
先說說異步IO
這個在Linux筆記里有,但是異步IO只有 Linux 下存在,在其他系統中沒有異步 IO 支持,那window的異步IO是怎么實現的,利用多線程,我們可以讓一個進程進行計算操作,另外一些進行 IO 調用,IO 完成后把信號傳給計算的線程,進而執行回調,這不就好了嗎?沒錯,異步 IO 就是使用這樣的線程池來實現的,只不過在不同的系統下面表現會有所差異,在 Linux 下可以直接使用線程池來完成,在Window系統下則采用 IOCP 這個系統API(其內部還是用線程池完成的)
上面的三個線程池都加粗了,因為他就是關鍵字,線程池的運行圖很常見
V8、事件循環、事件隊列都在單線程中運行,最右側還有工作線程(Worker Thread)負責提供異步的I/O操作,這就是為什么說Node.js擁有非阻塞的,事件驅動的異步IO架構
不僅是異步IO運行在線程池,NodeJS的計時器,http請求,瀏覽器的計時器,http請求ajax,ui渲染也都是運行在線程池的,也就是說js是單線程運行是錯的,他是同步任務單線程運行,在【Linux/IO】筆記里把NodeJS比作餐廳是最簡單的理解,他有個問題是菜做好了通知服務生來拿,IO執行完是不會通知服務生來拿的,正在的通知是線程池里的線程做的,也就是說服務生拿了菜單到廚房后,放了一招【影分身之術】,叫了一個線程在門口等着【上圖的觀察者】,菜做好了影分身喊了一句菜做好了,然后自己就消失了,這是主線程服務生才知道才做好了
原理代碼
/**
* 定義事件隊列
* 入隊:push()
* 出隊:shift()
* 空隊列:length == 0
*/
var globalEventQueue = []
/**
* 接收用戶請求
* 每一個請求都會進入到該函數
* 傳遞參數request和response
*/
function processHttpRequest(request,response){
// 定義一個事件對象
var event = createEvent({
params:request.params, // 傳遞請求參數
result:null, // 存放請求結果
callback:function(){} // 指定回調函數
});
// 在隊列的尾部添加該事件
globalEventQueue.push(event);
}
/**
* 事件循環主體,主線程擇機執行
* 循環遍歷事件隊列
* 處理非IO任務
* 處理IO任務
* 執行回調,返回給上層
*/
function eventLoop(){
// 如果隊列不為空,就繼續循環
while(this.globalEventQueue.length > 0){
// 從隊列的頭部拿出一個事件
var event = this.globalEventQueue.shift();
// 如果是耗時任務
if(isIOTask(event)){
// 從線程池里拿出一個線程
var thread = getThreadFromThreadPool();
// 交給線程處理
thread.handleIOTask(event)
}else {
// 非耗時任務處理后,直接返回結果
var result = handleEvent(event);
// 最終通過回調函數返回給V8,再由V8返回給應用程序
event.callback.call(null,result);
}
}
}
/**
* 處理IO任務
* 完成后將事件添加到隊列尾部
* 釋放線程
*/
function handleIOTask(event){
//當前線程
var curThread = this;
// 操作數據庫
var optDatabase = function(params,callback){
var result = readDataFromDb(params);
callback.call(null,result)
};
// 執行IO任務
optDatabase(event.params,function(result){
// 返回結果存入事件對象中
event.result = result;
// IO完成后,將不再是耗時任務
event.isIOTask = false;
// 將該事件重新添加到隊列的尾部
this.globalEventQueue.push(event);
// 釋放當前線程
releaseThread(curThread)
})
}
MicroTask
這個詞的中國名字叫微任務,這個概念是跟着Promise
一起出現的,百度Promise都會提到他解決了回調地獄
// 之前
fs.readFile('1.json', (err, data) => {
fs.readFile('2.json', (err, data) => {
fs.readFile('3.json', (err, data) => {
fs.readFile('4.json', (err, data) => {
});
});
});
});
// 現在
readFilePromise('1.json').then(data => {
return readFilePromise('2.json')
}).then(data => {
return readFilePromise('3.json')
}).then(data => {
return readFilePromise('4.json')
});
Promise確實是解決了回調地獄,但這只是改變了寫法,在沒有Promise的時代,代碼也一樣運行,那Promise到底帶來了什么,微任務帶來了什么,帶來了宏任務,233333,上面的EventLoop就是宏任務的運行規則,在沒有微任務的時候就是這么循環執行的,但是看上面的模擬運行,異步回調被放在了執行棧數組的最后面,倘若現在的任務隊列非常長,那么回調遲遲得不到執行,造成應用卡頓,於是他們開辟了微任務隊列,也就是第二個數組
- 一開始整段腳本作為第一個宏任務執行
- 執行過程中同步代碼直接執行,宏任務進入宏任務隊列,微任務進入微任務隊列
- 當前宏任務執行完出隊,檢查微任務隊列,如果有則依次執行,直到微任務隊列為空
- 執行瀏覽器 UI 線程的渲染工作
- 檢查是否有Web worker任務,有則執行
- 執行隊首新的宏任務,回到2,依此循環,直到宏任務和微任務隊列都為空
放到微任務隊列怎么理解呢
把下面的代碼運行一下,再把注釋解開運行一下
正常來說第一次是 1 2 3 4 2.1,因為400ms的計數器,等他回到微任務隊列,0ms的計數器都執行完了
正常來說第二次是 1 2 3 ... 2.1 4,當0ms的定時器返回,循環還在繼續,循環快完的時候,400ms的定時器也返回了,這時4是在2.1之前的,但是還是2.1比4先輸出,因為他插隊了,在微任務隊列了實現了插隊
console.log(1)
new Promise(function(x,y){
console.log(2)
setTimeout(()=>{
console.log(2.1)
},400)
}).then(x=>{
console.log(x)
})
console.log(3)
// for(var i=3;i<10000;i++){
// console.log(i)
// }
setTimeout(()=>{
console.log(4)
})
在瀏覽器是上面這么執行的,而NodeJS還在循環結束加了個nextTick函數,這是必須在微任務執行隊列執行完后執行的,也就是第三個數組,Vue也有一個nextTick是在異步的更新dom后執行的,模仿nodejs的執行概念
就這個理解面試應該沒問題了吧,廣州有沒有招人的,年后想換工作,求收留