總是一知半解的Event Loop

前端深夜告解室發表於2019-03-04

作者:孫輝,美團金融前端團隊成員。15年畢業加入美團,相信技術,更相信技術只是大千世界裡知識的一種,個人部落格: sunyuhui.com

前言

JavaScript中的事件迴圈一直都是一個很多人都或多或少了解,但說不清楚的知識點,停留在一知半解的層面。以前只需要使用回撥函式、定時器還好說,但是自從有了Promise之後,對事件迴圈的透徹瞭解就比較重要了。

本篇文章不打算從頭開始敘述,那樣篇幅太長,略過最基本的概念,我們簡單粗暴的把事件迴圈說清楚。

理論:關於MacroTask和MicroTask

一張圖展示JavaScript中的事件迴圈:

總是一知半解的Event Loop

一次事件迴圈:先執行macroTask佇列中的一個,然後執行microTask佇列中的所有任務。接著開始下一次迴圈(只是針對macroTask和microTask,一次完整的事件迴圈會比這個複雜的多)。

其中macroTaskmicroTask是兩種任務佇列,相比而言,大家更熟悉的一個詞是任務佇列task queue,其實就是macroTask),大家更熟悉的關於事件迴圈的機制說法大概是:主程式執行完了之後,每次從任務佇列裡取一個任務執行。但是promise出現之後,這個說法就不太準確了。

JavaScript引擎對這兩種佇列有不同的處理,簡單的說就是引擎會把我們的所有任務分門別類,一部分歸為macroTask,另外一部分歸為microTask,下面是類別劃分:

  • macroTask: setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering
  • microTask: process.nextTick, Promise, Object.observe, MutationObserver

我們所熟悉的定時器就屬於macroTask,但是僅僅瞭解macroTask的機制還是不夠的。

上面這些都是理論啊,我們怎麼直觀的感受兩種佇列的區別呢?說的再多也不如來一段實踐,我們感知這種區別最好的方式就是通過任務的執行順序

實踐:小二,上程式碼

我們以setTimeoutprocess.nextTickpromise為例直觀感受下兩種任務佇列的執行方式。

console.log(`main1`);

process.nextTick(function() {
    console.log(`process.nextTick1`);
});

setTimeout(function() {
    console.log(`setTimeout`);
    process.nextTick(function() {
        console.log(`process.nextTick2`);
    });
}, 0);

new Promise(function(resolve, reject) {
    console.log(`promise`);
    resolve();
}).then(function() {
    console.log(`promise then`);
});

console.log(`main2`);複製程式碼

彆著急看答案,先以上面的理論自己想想,執行結果會是啥?

來個圖片佔個位

總是一知半解的Event Loop

最終結果是這樣的:

main1
promise
main2
process.nextTick1
promise then
setTimeout
process.nextTick2複製程式碼

process.nextTickpromise thensetTimeout 前面輸出,已經證明了macroTaskmicroTask的執行順序。但是有一點必須要指出的是。上面的圖容易給人一個錯覺,就是主程式的程式碼執行之後,會先呼叫macroTask,再呼叫microTask,這樣在第一個迴圈裡一定是macroTask在前,microTask在後。

但是最終的實踐證明:在第一個迴圈裡,process.nextTick1promise then這兩個microTask是在setTimeout這個macroTask裡之前輸出的,這是為什麼呢?

因為主程式的程式碼也屬於macroTask(這一點我比較疑惑的是主程式都是一些同步程式碼,而macroTask和microTask包含的都是一些非同步任務,為啥主程式的程式碼會被劃分為macroTask,不過從實踐來看確實是這樣,而且也有理論支撐:【翻譯】Promises/A+規範)。

主程式這個macroTask(也就是main1promisemain2)執行完了,自然會去執行process.nextTick1promise then這兩個microTask。這是第一個迴圈。之後的setTimeoutprocess.nextTick2屬於第二個迴圈

別看上面那段程式碼好像特別繞,把原理弄清楚了,都一樣 ~

requestAnimationFrameObject.observe(已廢棄)MutationObserver這三個任務的執行機制大家可以從上面看到,不同的只是具體用法不同。重點說下UI rendering。在HTML規範:event-loop-processing-model裡敘述了一次事件迴圈的處理過程,在處理了macroTaskmicroTask之後,會進行一次Update the rendering,其中細節比較多,總的來說會進行一次UI的重新渲染。

後續

不知道大家有沒有發現一個現象,在學習技術點的時候,如果太淺,得來的知識點可能不完整甚至是錯的,如果追究的太深,又會給人一種太偏學究的感覺,其中的平衡點,大家自己留心把握。

done

另外,大家端午快樂 ~

最後,團隊為了招聘方便,整了個公眾號,主要是一些招聘資訊,團隊資訊,所有的技術文章在公眾號裡也可以看到,對了,如果你想去美團其他團隊,我們也可以幫你內推哦 ~

二維碼
二維碼

參考:

  1. HTML規範:event-loop-processing-model
  2. 【翻譯】Promise/A規範

相關文章