事件循環機制

將本身讀到的比較好的文章分享出來,你們互相學習,各位大佬有好的文章也能夠留個連接互相學習,萬分感謝!

線程與進程

關於線程與進程的關係能夠用下面的圖進行說明:ios

clipboard.png

  • 進程比如圖中的工廠,有單獨的專屬本身的工廠資源。
  • 線程比如圖中的工人,多個工人在一個工廠中協做工做,工廠與工人是 1:n的關係。
  • 多個工廠之間獨立存在。

而官方的說法是:git

  • 進程是 CPU資源分配的最小單位。
  • 線程是 CPU調度的最小單位。

從更直觀的例子來看,能夠打開任務管理器查看,第一個 tab即是進程列表,每個進程佔有的 CPU資源和內存資源的比例很直觀的展現出來。github

clipboard.png

爲何js是單線程

初學計算機語言的時候,不管是 C、C++仍是 JAVA,都是支持多線程,恰恰 JavaScript是單線程,不支持多線程,這也跟 JavaScript的做用有關,都知道 JavaScript是主要運行在瀏覽器的腳本語言,最終操做的是頁面的 DOM結構,當兩個 JavaScript腳本同時修改頁面的同一個 DOM節點時,瀏覽器該執行哪一個呢?因此當時設計 JavaScript時,便要求當前修改操做完成後方可進行下一步修改操做。ajax

瀏覽器是支持多進程

一樣咱們打開瀏覽器的任務管理器,如下圖爲例:瀏覽器

clipboard.png

瀏覽器的每個 tab頁都是一個進程,有對應的內存佔用空間、 CPU使用量以及進程ID。 新打開一個 tab頁時,都會新建一個進程,因此就有一個 tab頁對應一個進程的說法,可是這種說法又是錯誤的,由於瀏覽器有本身的優化機制,當咱們打開多個空白的 tab頁時,瀏覽器會將這多個空白頁的進程合併爲一個,從而減小了進程的數量個數。多線程

瀏覽器內核

瀏覽器內核中有多個進程在同步工做,今天涉及到的瀏覽器的進程主要包括如下進程:異步

  • Browser 進程

主進程,主要負責頁面管理以及管理其餘進程的建立和銷燬等,常駐的線程有:函數

  • GUI渲染線程
  • JS引擎線程
  • 事件觸發線程
  • 定時器觸發線程
  • HTTP請求線程

GUI渲染線程oop

  • 主要負責頁面的渲染,解析HTML、CSS,構建DOM樹,佈局和繪製等。
  • 當界面須要重繪或者因爲某種操做引起迴流時,將執行該線程。
  • 該線程與JS引擎線程互斥,當執行JS引擎線程時,GUI渲染會被掛起,當任務隊列空閒時,JS引擎纔會去執行GUI渲染。

JS引擎線程佈局

  • 該線程固然是主要負責處理 JavaScript腳本,執行代碼。
  • 也是主要負責執行準備好待執行的事件,即定時器計數結束,或者異步請求成功並正確返回時,將依次進入任務隊列,等待 JS引擎線程的執行。
  • 固然,該線程與 GUI渲染線程互斥,當 JS引擎線程執行 JavaScript腳本時間過長,將致使頁面渲染的阻塞。

事件觸發線程

  • 主要負責將準備好的事件交給 JS引擎線程執行。
  • 好比 setTimeout定時器計數結束, ajax等異步請求成功並觸發回調函數,或者用戶觸發點擊事件時,該線程會將整裝待發的事件依次加入到任務隊列的隊尾,等待 JS引擎線程的執行。

定時器觸發線程

  • 顧名思義,負責執行異步定時器一類的函數的線程,如: setTimeout,setInterval
  • 主線程依次執行代碼時,遇到定時器,會將定時器交給該線程處理,當計數完畢後,事件觸發線程會將計數完畢後的事件加入到任務隊列的尾部,等待JS引擎線程執行。

HTTP請求線程

  • 顧名思義,負責執行異步請求一類的函數的線程,如: Promise,anxios,ajax等。
  • 主線程依次執行代碼時,遇到異步請求,會將函數交給該線程處理,當監聽到狀態碼變動,若是有回調函數,事件觸發線程會將回調函數加入到任務隊列的尾部,等待JS引擎線程執行。

多個線程之間配合工做,各司其職。

  • Render 進程
瀏覽器渲染進程(瀏覽器內核),主要負責頁面的渲染、JS執行以及事件的循環。

同步任務和異步任務

  • 同步任務 便可以當即執行的任務,例如 console.log() 打印一條日誌、聲明一個變量或者執行一次加法操做等。
  • 異步任務 相反不會當即執行的事件任務。異步任務包括宏任務微任務(後面會進行解釋~)。
  • 常見的異步操做:

    • Ajax
    • DOM的事件操做
    • setTimeout
    • Promise的then方法
    • Node的讀取文件

下圖給出了同步任務與異步任務的執行流程:

clipboard.png

  • 就像是一個容器,任務都是在棧中執行。
  • 主線程 就像是操做員,負責執行棧中的任務。
  • 任務隊列 就像是等待被加工的物品。
  • 異步任務完成註冊後會將回調函數加入任務隊列等待主線程執行。
  • 執行棧中的同步任務執行完畢後,會查看並讀取任務隊列中的事件函數,因而任務隊列的函數結束等待狀態,進入執行棧,開始執行。

那麼任務究竟是如何入棧和出棧的呢?能夠用一小段代碼進行解釋。

入棧與出棧

如下面的代碼爲例:

console.log(1);
    function fn1(){
        console.log(2);
    }
    function fn2(){
        console.log(3);
        fn1();
    }
    setTimeout(function(){
        console.log(4);
    }, 2000);
    fn2();
    console.log(5);

clipboard.png

因此上面代碼運行的結果爲:1,3,2,5,4。

宏任務和微任務

異步任務分爲宏任務和微任務,宏任務隊列能夠有多個,微任務隊列只有一個。

宏任務和微任務的執行方式在瀏覽器和 Node 中有差別。

宏任務(macrotask)

script(全局任務), setTimeoutsetIntervalsetImmediateI/OUI rendering

微任務(macrotask)

process.nextTickPromise.then()Object.observeMutationObserver

在微任務中 process.nextTick 優先級高於Promise

當一個異步任務入棧時,主線程判斷該任務爲異步任務,並把該任務交給異步處理模塊處理,當異步處理模塊處理完打到觸發條件時,根據任務的類型,將回調函數壓入任務隊列。

  • 若是是宏任務,則新增一個宏任務隊列,任務隊列中的宏任務能夠有多個來源。
  • 若是是微任務,則直接壓入微任務隊列。

因此上圖的任務隊列能夠繼續細化一下:

clipboard.png

那麼當棧爲空時,宏任務和微任務的執行機制又是什麼呢?

Event Loop

到這裏,除了上面的問題,咱們已經把事件循環的最基本的處理方式搞清楚了,但具體到異步任務中的宏任務和微任務,尚未弄明白。咱們能夠先順一遍執行機制:

  • 從全局任務 script開始,任務依次進入棧中,被主線程執行,執行完後出棧。
  • 遇到異步任務,交給異步處理模塊處理,對應的異步處理線程處理異步任務須要的操做,例如定時器的計數和異步請求監聽狀態的變動。
  • 當異步任務達到可執行狀態時,事件觸發線程將回調函數加入任務隊列,等待棧爲空時,依次進入棧中執行。

到這問題就來了,當異步任務進入棧執行時,是宏任務仍是微任務呢?

  • 因爲執行代碼入口都是全局任務 script,而全局任務屬於宏任務,因此當棧爲空,同步任務任務執行完畢時,會先執行微任務隊列裏的任務。
  • 微任務隊列裏的任務所有執行完畢後,會讀取宏任務隊列中拍最前的任務。
  • 執行宏任務的過程當中,遇到微任務,依次加入微任務隊列。
  • 棧空後,再次讀取微任務隊列裏的任務,依次類推。

實例解析

回到最開始的那段代碼,如今咱們能夠一步一步的看一下執行順序。

console.log(1);
setTimeout(function(){
    console.log(2);
}, 0);
setTimeout(function(){
    console.log(3)
},2000)
console.log(4);
  • 從全局任務入口,首先打印日誌 1
  • 遇到宏任務 setTimeout,交給異步處理模塊,咱們暫且先記爲 setTimeout1
  • 再次遇到宏任務 setTimeout,交給異步處理模塊,咱們暫且先記爲 setTimeout2
  • 順序執行,打印日誌 4
  • 此時同步任務已執行完畢,讀取宏任務隊列的任務,先執行 setTimeout1的回調函數,由於定時器的等待時間爲 0秒,因此會直接輸出 2,可是 W3CHTML標準中規定,規定要求 setTimeout中低於 4ms的時間間隔算爲 4ms
  • 因爲瀏覽器在執行以上三步時,並未耗時好久,因此當宏任務 setTimeout1執行完時, setTimeout2的等待時間並未結束,因此在 2秒後打印日誌 3,實際上並未等待2秒。

下面咱們能夠再看一個實例:

setTimeout(function(){
        console.log(1);
        Promise.resolve().then(function(){
            console.log(2)
        })
    },0)

    setTimeout(function(){
        console.log(3)
    },0)
    Promise.resolve().then(function(){
        console.log(4)
    });
    console.log(5)

當代碼中遇到了異步請求的事件,又該如何執行,根據上面總結的執行機制,又該獲得什麼樣的結果?

第一輪循環

  • 一樣從全局任務入口,遇到宏任務 setTimeout,交給異步處理模塊,咱們暫且先記爲 setTimeout1,因爲等待時間爲 0,直接加入宏任務隊列。
  • 再次遇到宏任務 setTimeout,交給異步處理模塊,咱們暫且先記爲 setTimeout2,一樣直接加入宏任務隊列。
  • 遇到微任務 then(),加入微任務隊列。
  • 最後遇到打印語句,直接打印日誌 5

第一輪循環結束後,能夠畫出下圖:

clipboard.png

第二輪循環

  • 棧空後,先執行微任務隊列中的 then()方法,輸出 4,此時微任務隊列爲空。

clipboard.png

  • 讀取宏任務隊列的最靠前的任務 setTimeout1
  • 先直接執行打印語句,打印日誌 1,又遇到微任務 then(),加入微任務隊列。第二輪循環結束。

clipboard.png

第三輪循環

  • 先執行微任務隊列中的 then()方法,輸出 2,此時微任務隊列爲空。

clipboard.png

  • 繼續讀取宏任務隊列的最靠前的任務 setTimeout2
  • 直接執行打印語句,打印日誌 3。第三輪循環結束,執行完畢。

clipboard.png

最後咱們是咱們的boss,歡迎你們在評論區留言寫出本身心中的那個正確答案。

console.log(1);

setTimeout(function(){
    console.log(2);
    new Promise(function(resolve, reject){
        console.log(3);
        resolve();
    }).then(function(){
        console.log(4);
    })
})

new Promise(function(resolve, reject){
    console.log(5);
    resolve();
}).then(function(){
    console.log(6);
})

setTimeout(function(){
    console.log(7)
})

setTimeout(function(){
    console.log(8);
    new Promise(function(resolve, reject){
        console.log(9);
        resolve();
    }).then(function(){
        console.log(10);
    })
})

new Promise(function(resolve){
    console.log(11);
    resolve();
}).then(function(){
    console.log(12)
})

console.log(13)

github地址:https://github.com/ABCDdouyaer/a_article_per_day/tree/master/0001
原文連接:https://mp.weixin.qq.com/s/9_hZX_xWSr3Gd1X_2_WOsA

相關文章
相關標籤/搜索