今天,我明白了JS事件循環機制

寫在前面

js是一門單線程的編程語言,也就是說js在處理任務的時候,全部任務只能在一個線程上排隊被執行,那若是某一個任務耗時比較長呢?總不能等到它執行結束再去執行下一個。
因此在線程以內,又被分爲了兩個隊列:html

  • 同步任務隊列
  • 異步任務隊列

舉個例子來講:好比你去銀行辦理業務,都須要領號排隊。銀行櫃員一個個辦理業務,這時這個櫃員就至關於一個js線程,客戶排的隊就至關於同步任務隊列,每一個人對於櫃員至關於一個個的任務。
但這個時候,你的電話忽然響了,你去接電話接了半小時。這時候人家櫃員一看你這狀況,直接叫了下一個,而你領的號就做廢了,只能從新零號排隊。這時候你就是被分發到了異步任務隊列。
等你前邊的人都完事了,櫃員把你叫過去辦了你的業務,這時候就是同步隊列中的任務執行完了,主線程會處理異步隊列中的任務。前端

同步任務和異步任務

這裏說的異步任務,它的意思是包含了獨立於主執行棧以外的宏任務和微任務html5

先看一個簡單的例子,對這樣的執行機制有個簡單的認識:面試

console.log('start')

console.log('end')

上邊的執行結果你們確定都明白,先輸出start,再輸出end,這一段代碼會進入同步隊列,順序執行。ajax

那麼咱們加點料:編程

console.log('start')

setTimeout(function() {
    console.log('setTimeout')
}, 0)

console.log('end')

這樣的狀況,函數調用棧執行到setTimeout時,setTimeout會在規定的時間點將回調函數放入異步隊列,等待同步隊列的任務被執行完,當即執行,因此結果是:start、end、setTimeout。promise

但須要注意的一點是,廣泛認爲setTimeout定時執行的認知是片面的,由於假設setTimeout規定2秒後執行,但同步隊列中有一個函數,執行花了很長時間,甚至花了1秒。那麼這時setTimeout中的回調也會等上至少1秒以後,同步任務都執行完了,再去執行。這時候的setTimeout回調執行的時機就會超過2秒,也就是至少3秒。異步

宏任務與微任務

宏任務與微任務都是獨立與主執行棧以外的另外兩個隊列,能夠在概念上劃分在異步任務隊列裏。而這些隊列由js的事件循環(EventLoop)來搞定編程語言

macro-task(宏任務)與micro-task(微任務),在最新標準中,它們被分別稱爲task與jobs。

因爲寫文章時沒有注意到,實際上宏任務與微任務的概念是不許確的,但因爲文章中涉及多處宏任務、微任務的解讀,因此本文暫時仍是用宏任務、微任務來分別代指task、jobs。但讀者要明白規範中沒有宏任務的概念,只有task與jobs函數

其中宏任務(task)包括:

  • script(總體代碼)
  • setTimeout, setInterval, setImmediate,
  • I/O
  • UI rendering

ajax請求不屬於宏任務,js線程遇到ajax請求,會將請求交給對應的http線程處理,一旦請求返回結果,就會將對應的回調放入宏任務隊列,等請求完成執行。

微任務(jobs)包括:

  • process.nextTick
  • Promise
  • Object.observe(已廢棄)
  • MutationObserver(html5新特性)

這些咱們能夠理解爲它們在執行上下文中都是可執行代碼,會當即執行,只不過會將各自的回調函數放入對應的任務隊列中(宏任務微任務),也就至關於一個調度者。

咱們梳理一下事件循環的執行機制:
循環首先從宏任務開始,遇到script,生成執行上下文,開始進入執行棧,可執行代碼入棧,依次執行代碼,調用完成出棧。
執行過程當中遇到上邊提到的調度者,會同步執行調度者,由調度者將其負責的任務(回調函數)放到對應的任務隊列中,直到主執行棧清空,而後開始執行微任務的任務隊列。微任務也清空後,再次從宏任務開始,一直循環這一過程。

示例

上邊說了那麼多,仍是用一些代碼來驗證一下是不是這樣的,先來一個簡單一點的。

console.log('start')

setTimeout(function() {
    console.log('timeout')
}, 0)

new Promise(function(resolve) {
    console.log('promise')
    resolve()
}).then(function() {
    console.log('promise resolved')
})

console.log('end')

根據上邊的結論,分析一下執行過程:

  • 創建執行上下文,進入執行棧開始執行代碼,打印start
  • 往下執行,遇到setTimeout,將回調函數放入宏任務隊列,等待執行
  • 繼續往下,有個new Promise,其回調函數並不會被放入其餘任務隊列,所以會同步地執行,打印promise,可是當resolve後,.then會把其內部的回調函數放入微任務隊列
  • 執行到了最底部的代碼,打印出end。這時,主執行棧清空了,開始尋找微任務隊列裏有沒有可執行代碼
  • 發現了微任務隊列中有以前放進去的代碼,執行打印出promise resolved,第一次循環結束
  • 再開始第二次循環,從宏任務開始,檢查宏任務隊列是否有可執行代碼,發現有一個,打印timeout

因此,打印順序是:start-->promise-->end-->promise resolved-->timeout

上邊是一個簡單示例,比較好理解。那麼接下來看一個稍微複雜一點的(這裏直接用漢字直觀地代表了打印的時機,避免看起來費勁):

console.log('第一次循環主執行棧開始')

setTimeout(function() {
    console.log('第二次循環開始,宏任務隊列的第一個宏任務執行中')
    new Promise(function(resolve) {
        console.log('宏任務隊列的第一個宏任務的微任務繼續執行')
        resolve()
    }).then(function() {
        console.log('第二次循環的微任務隊列的微任務執行')
    })
}, 0)

new Promise(function(resolve) {
    console.log('第一次循環主執行棧進行中...')
    resolve()
}).then(function() {
    console.log('第一次循環微任務,第一次循環結束')
    setTimeout(function() {
        console.log('第二次循環的宏任務隊列的第二個宏任務執行')
    })
})

console.log('第一次循環主執行棧完成')

一樣咱們分析一下執行過程:

  • 第一次循環

    1. 進入執行棧執行代碼,打印第一次循環主執行棧開始
    2. 遇到setTimeout,將回調放入宏任務隊列等待執行
    3. promise聲明過程是同步的,打印第一次循環主執行棧進行中...,resolve後遇到.then,將回調放入微任務隊列
    4. 打印第一次循環主執行棧完成
    5. 檢查微任務隊列是否有可執行代碼,有一個第三步放入的任務,打印第一次循環微任務,第一次循環結束,第一次循環結束,同時遇到setTimeout,將回調放入宏任務隊列
  • 第二次循環

    1. 從宏任務入手,檢查宏任務隊列,發現有兩個宏任務,分別是第一次循環第二步和第一次循環第五步被放入的任務,先執行第一個宏任務,打印第二次循環開始,宏任務隊列的第一個宏任務執行中
    2. 遇到promise聲明語句,打印宏任務隊列的第一個宏任務繼續執行,這時候又被resolve了,又會將.then中的回調放入微任務隊列,這是這個宏任務隊列中的第一個任務還沒執行完
    3. 第一個宏任務中的同步代碼執行完畢,檢查微任務隊列,發現有一段第二步放進去的代碼,執行打印第二次循環的微任務隊列的微任務執行,此時第一個宏任務執行完畢
    4. 開始執行第二個宏任務,打印第二次循環的宏任務隊列的第二個宏任務執行,全部任務隊列所有清空,執行完畢

因此打印順序爲:

  • 第一次循環主執行棧開始
  • 第一次循環主執行棧進行中...
  • 第一次循環主執行棧完成
  • 第一次循環微任務,第一次循環結束
  • 第二次循環開始,宏任務隊列的第一個宏任務執行中
  • 第二次循環的宏任務隊列的第一個宏任務的微任務繼續執行
  • 第二次循環的微任務隊列的微任務執行
  • 第二次循環的宏任務隊列的第二個宏任務執行

看一下gif,事件循環以肉眼可見的形式呈現出來(兩次循環之間有微小的時間間隔)

圖片描述

總結

js的執行機制是面試中常考的點,也是很是繞的。但相信徹底瞭解事件循環機制,仔細分析的話,面試遇到這樣的題徹底不是問題。我在寫這篇文章的時候,發現本身以前理解的很大一部分是錯的。若是你們以爲哪裏有錯誤,還請幫忙指點出來。

歡迎關注個人公衆號: 一口一個前端,不按期分享我所理解的前端知識

相關文章
相關標籤/搜索