EventLoop


文章資料來自
Node.js 事件循環機制
JS靈魂之問(下)

EventLoop的中國名字叫事件循環,這個玩意真的是高深莫測,一般開發都用不到,代碼只管寫就行,雖然不用懂,但是面試就是要問,這對我這種小菜雞真是滿滿的惡意

先說說異步IO
這個在Linux筆記里有,但是異步IO只有 Linux 下存在,在其他系統中沒有異步 IO 支持,那window的異步IO是怎么實現的,利用多線程,我們可以讓一個進程進行計算操作,另外一些進行 IO 調用,IO 完成后把信號傳給計算的線程,進而執行回調,這不就好了嗎?沒錯,異步 IO 就是使用這樣的線程池來實現的,只不過在不同的系統下面表現會有所差異,在 Linux 下可以直接使用線程池來完成,在Window系統下則采用 IOCP 這個系統API(其內部還是用線程池完成的)

上面的三個線程池都加粗了,因為他就是關鍵字,線程池的運行圖很常見

image.png

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就是宏任務的運行規則,在沒有微任務的時候就是這么循環執行的,但是看上面的模擬運行,異步回調被放在了執行棧數組的最后面,倘若現在的任務隊列非常長,那么回調遲遲得不到執行,造成應用卡頓,於是他們開辟了微任務隊列,也就是第二個數組

  1. 一開始整段腳本作為第一個宏任務執行
  2. 執行過程中同步代碼直接執行,宏任務進入宏任務隊列,微任務進入微任務隊列
  3. 當前宏任務執行完出隊,檢查微任務隊列,如果有則依次執行,直到微任務隊列為空
  4. 執行瀏覽器 UI 線程的渲染工作
  5. 檢查是否有Web worker任務,有則執行
  6. 執行隊首新的宏任務,回到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的執行概念

就這個理解面試應該沒問題了吧,廣州有沒有招人的,年后想換工作,求收留


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM