javascript的執行機制—Event Loop

既然今天要談的是javascript的事件循環機制,要理解事件循環,首先要知道事件循環是什麼。javascript

咱們先從一個例子來看一下javascript的執行順序。html

<script>
    setTimeout(function() {
        console.log('定時器開始了.');
    },0)
    
    new Promise(function(resolve) {
        console.log('立刻執行for循環了');
        for (let i = 0; i < 10000; i++) {
            i == 99 && resolve();
        }
    }).then(function() {
        console.log('執行then函數了');
    })
    
    console.log('代碼執行結束');
    //執行結果爲:
    //立刻執行for循環了
    //代碼執行結束
    //執行then函數了
    //定時器開始了.
</script>

怎麼樣,是否是和本身在內心運行的結果差了一萬八千里呢。若是是的話,請耐心看完後面的內容,讓你完全弄明白javascript的事件循環機制。java

單線程的javascript

要想了解事件循環的咱們就得從javascript的工做原理開始提及。程序員

javascript語言的一大特色就是單線程,但是爲何javascript不作成多線程呢?ajax

JavaScript的單線程,與它的用途有關。做爲瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操做DOM。這決定了它只能是單線程,不然會帶來很複雜的同步問題。好比,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另外一個線程刪除了這個節點,這時瀏覽器應該以哪一個線程爲準?編程

任務隊列

咱們說單線程就意味着全部的任務必須排隊。就相似於銀行只有一個窗口,前一個任務執行結束後,後一個任務才能執行。若是新執行的任務耗時很長,那麼後一個任務就不得不一直等着。瀏覽器

這樣就又出現了一個問題,在進行瀏覽器的操做時,咱們經常會經過ajax向後臺發送請求,然而js必須等到瀏覽器接收到響應內容後纔會繼續往下執行,若是這段時間是10s,那麼頁面必須停在這裏10s。這不只會影響用戶體驗,也會下降CPU的利用率,顯然不是咱們想要的。多線程

因而,聰明的程序員小哥哥就把任務分紅了兩類異步

  • 同步任務
  • 異步任務

同步任務指的是:在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;異步編程

異步任務指的是:不進入主線程、而進入其餘線程的任務(好比處理事件的事件觸發線程,處理HTTP請求的異步HTTP請求線程,處理定時器的定時器觸發線程),當任務完成後,相應的線程會把對應的回調函數放置到任務隊列中,一旦執行棧中的全部同步任務執行完畢(此時JS引擎空閒),系統就會讀取任務隊列,將可運行的異步任務添加到可執行棧中,開始執行。

同步任務和異步任務的執行過程大體能夠簡化成以下的導圖所示。

 

 

  • 同步和異步任務分別進入不一樣的執行"場所",同步的進入主線程,異步的進入Event Table並註冊函數。
  • 當指定的事情完成時,Event Table會將這個函數移入Event Queue。
  • 主線程內的任務執行完畢爲空,會去Event Queue讀取對應的函數,進入主線程執行。
  • 上述過程會不斷重複,也就是常說的Event Loop(事件循環)。
爲了便於理解事件循環,咱們來看一段代碼。
<script>
    console.log(1);
    setTimeout(function task() { 
    console.log('定時器執行了.');
  },
1000);
  console.log(
2); </script>
  • js代碼從上往下依次執行,
  • 遇到console.log(1),執行並打印出來。.
  • 遇到異步任務setTimeout,task進入Event Table並註冊,計時開始。
  • 遇到console.log(2),執行並打印出來。
  • 主線程執行完畢,開始查詢任務隊列有沒有等待執行的回調函數。
  • 一秒鐘到後,timeout計時事件完成,task進入Event Queue。
  • 主線程發現任務隊列有等待執行的函數task,將task調進進入主線程執行。

咱們不由要問了,那怎麼知道主線程執行棧爲空啊?js引擎存在monitoring process(這個不知道翻譯成啥好,由於翻譯成監聽進程也不對),會持續不斷的檢查主線程執行棧是否爲空,一旦爲空,就會去Event Queue那裏檢查是否有等待被調用的函數。

macrotask 與 microtask

其實除了廣義的同步任務和異步任務的劃分,異步任務還能夠細分爲宏任務(macrotask) 與微任務( microtask)。不一樣的異步任務類型會進入不一樣的Event Queue。

宏任務: 須要屢次事件循環才能執行完,事件隊列中的每個事件對應的回調函數都是一個宏任務,每次事件循環都會調入一個宏任務;

瀏覽器爲了可以使得js內部宏任務與DOM任務有序的執行,會在一個宏任務執行結束後,在下一個宏執行開始前,對頁面進行從新渲染 (task->渲染->task->…)。

例如鼠標點擊會觸發一個事件回調,須要執行一個宏任務,而後從新渲染頁面;setTimeout的做用是等待給定的時間後爲它的回調產生一個新的宏任務。

微任務: 微任務是一次性執行完的。微任務一般來講是須要在當前task執行結束後當即執行的任務,例如對一些動做作出反饋或者異步執行任務又不須要分配一個新的task,這樣即可以提升一些性能。只要執行棧中沒有其餘的js代碼正在執行了,並且當前調入的宏任務都執行完了,微任務隊列會當即執行。若是在微任務執行期間微任務隊列加入了新的微任務,會將新的微任務加入隊列尾部,以後也會被執行。

    • 也就是說微任務的執行,在當前task任務後,下一個task以前,在渲染以前
    • 因此它的響應速度相比setTimeout(setTimeout是task)會更快,由於無需等渲染
    • 也就是說,在某一個macrotask執行完後,就會將在它執行期間產生的全部microtask都執行完畢(在渲染前)

簡單理解,宏任務在下一輪事件循環執行,微任務在本輪事件循環的全部任務結束後從新渲染前執行

  • 宏任務主要包括了:setTimeout、setInterval、setImmediate、I/O、各類事件(好比鼠標單擊事件)的回調函數
  • 優先級:主代碼塊 > setImmediate > MessageChannel > setTimeout / setInterval
  • 微任務主要包括了:process.nextTick、Promise、MutationObserver
  • 優先級:process.nextTick > Promise > MutationObserver

據whatwg規範介紹:

  • 一個事件循環(event loop)會有一個或多個任務隊列(task queue)
  • 每個 event loop 都有一個 macrotask queue
  • task queue == macrotask queue != microtask queue
  • 一個任務 task 能夠放入 macrotask queue 也能夠放入 microtask queue 中
  • 調用棧清空(只剩全局),而後執行全部的microtask。當全部可執行的microtask執行完畢以後。循環再次從macrotask開始,找到其中一個宏任務執行完畢,若是這個宏任務中可能包含宏任務或微任務,會將宏任務添加到事件隊列中,而後再執行全部的microtask,這樣一直循環下去。

宏任務、微任務執行流程圖以下所示。

這時,咱們再來看一下文章開頭給的一段代碼。

<script>
    setTimeout(function() {
        console.log('定時器開始了.');
    },0)
    
    new Promise(function(resolve) {
        console.log('立刻執行for循環了');
        for (let i = 0; i < 10000; i++) {
            i == 99 && resolve();
        }
    }).then(function() {
        console.log('執行then函數了');
    })
    
    console.log('代碼執行結束');
    //執行結果爲:
    //立刻執行for循環了
    //代碼執行結束
    //執行then函數了
    //定時器開始了.
</script>

執行步驟以下所示。

  • 當頁面首次加載時,<script>標籤內的代碼段做爲一個宏任務進入主線程,依次向下執行。
  • 遇到setTimeout將回調函數註冊後壓入宏任務的事件隊列。
  • 遇到new Promise當即執行,輸出'立刻執行for循環了'。將then函數壓入到微任務隊列。
  • 遇到console.log('代碼執行結束'),執行代碼。輸出"代碼執行結束了"。
  • 主線程執行完後,先檢查微任務隊列中有沒有待執行的任務,發現then函數在微任務隊列裏,將其取出到主線程執行,輸出"執行then函數了"。
  • 開始下一輪事件循環,從紅任務隊列中取出setTimeout事件的回調函數,執行。輸出「定時器開始了」。
  • 結束。

總結

javascript是一門單線程的語言,事件循環是js異步編程的一種方法。也是js的執行機制。當瀏覽器中的網頁剛剛載入的時候,<script>裏的代碼會做爲第一個宏任務被壓入棧執行,同步代碼執行完後,若是有微任務就執行微任務,沒有微任務就執行下一個宏任務。如此往復循環,直至全部任務都執行完畢。

參考文章

JavaScript 運行機制詳解:再談Event Loop

瀏覽器內的事件隊列

這一次,完全弄懂 JavaScript 執行機制

相關文章
相關標籤/搜索