iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 11
0
Modern Web

從技術文章深入學習 JavaScript系列 第 11

Day 11 [EventLoop 01] 一次弄懂Event Loop(徹底解決此類面試問題)

文章選自

作者:光光同学22167

連接:https://juejin.im/post/6844903764202094606

來源:掘金

開始

注意以下僅分析瀏覽器部分,Node的EventLoop暫時跳過

甚麼是事件循環

JS是一門單線程語言

單線程語言有甚麼問題,就是一次只能處理一件事,就像是去郵局裡面的一個窗口,一次只能處理一個客戶。但這會導致一個問題:

假設今天我們向服務端請求一個圖片,但請求了很長一段時間,那我們畫面不就卡住了

因此這裡程序員很聰明地將任務分成兩大類

  1. 同步任務
  2. 異步任務

EventLoop

即事件循環,指瀏覽器或是node解決JS單線程阻塞的解決機制(注意兩種不一樣)

宏任務與微任務

在JavaScript裡面任務分成兩種

  1. 宏任務 (MacroTask):
    • script全部代碼
    • setTimeout
    • setInterval
    • setImmediate (瀏覽器通常不支持(只有IE10支持))
    • I/O
    • UI Rendering
  2. 微任務 (MicroTask):
    • Process.nextTick(Node才有)
    • Promise (注意executer是同步)
    • Object.observe(廢了)
    • MutationObserver

瀏覽器的EventLoop

Javascript有一個main thread主線程和call-stack調用棧(執行棧)

所有的任務都會被放到調用棧等待主線程執行。

EventLoop分析

  1. 執行棧在執行完同步任務後,查看執行棧是否為空
  2. 如果執行棧為空,就會去檢查微任務隊列是否為空,若不為空執行所有微任務
  3. 如果微任務對列為空,執行宏任務
  4. 每次單個宏任務執行完畢後,檢查微任務隊列是否為空,如果不為空按先入先出規則,全部執行完微任務
  5. 設置為任務隊列成null,然後執行宏任務,這樣一直循環

例子

console.log('script start');

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

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});
console.log('script end');

第一次執行

.首次執行

MacroTask:

  • run script
  • setTimeout callback

MicroTask:

  • Promise1 then

JS Stack(調用棧) : script(第一次執行程序)

https://ithelp.ithome.com.tw/upload/images/20200921/20124350EGU5FDMSE1.png

第二次執行

率先查看微任務隊列是否為空,此例還有Promise then(分別是)存在,所以率先執行

  1. 先執行Promise1 then
  2. 執行完後,又調用了Promise2 then,放進為任務隊列裡
  3. 執行Promise2 then

以下是執行Promise2 then的情況

MacroTask:

  • run script

  • setTimeout callback

Microtasks:

  • Promise2 then

JS stack:Promise2 callback

https://ithelp.ithome.com.tw/upload/images/20200921/201243509pxfpHS0kD.png

第三次執行:

MacroTasks:

  • setTimeout callback

Microtasks:

JS stack: setTimeout callback

https://ithelp.ithome.com.tw/upload/images/20200921/20124350iNg4qlI7C7.png

第四次執行:

清空MacroTasks隊列和JS stack

MacroTasks:

Microtasks:

JS stack:

附註:

  1. 上面那個例子可以到這個網站觀看

    https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

  2. 這張圖可以幫助思考

  3. 注意Promise的鍊式調用

    比方說這個例子

    let outerData = 0
    Promise.resolve().then(function (data) {
      console.log('promise1');
      new Promise((resolve, reject) => {
        resolve(5)
      }).then(data => { // 鍊式調用需要等上一個then處理完
        outerData = data
        console.log('promise12');
      })
    })
    Promise.resolve().then(function () {
      console.log(outerData); // 這裡很明顯會打印出0(因為會比promise12還早進隊列)
      console.log('promise2');
    });
    console.log('script end');
    
  4. Promise的executor是同步的(Promise的callback)

    new Promise(resolve => {
      console.log('我在第一喔') 
      resolve()
    }).then(data => {
      console.log('then');
    })
    
    console.log('我居然在第二');
    

    https://ithelp.ithome.com.tw/upload/images/20200921/20124350QkKBIfO8KC.png

例子二

前提知識

  • async/await (其實是Promise的語法糖)

    在底層轉換了Promise跟then的函數

    每次我們使用await,解釋器都創建一個promise對象,然後把剩下的async函數中的操作放到then回調函數中。

    async/await的實現,離不開Promise。從字面意思來理解,async是“異步”的簡寫,而awaitasync wait的簡寫可以認為是等待異步方法執行完成。

    賀老師知乎上的一個例子 :

    // 可以把這個例子
    async function f() {
      await p
      console.log('ok')
    }
    // 想像成
    function f() {
      return RESOLVE(p).then(() => {
        console.log('ok')
      })
    }
    
    

    分析:

    1. await後面的p:

      因為resolve裡面放的是同步函數(所以p會立即執行)

    2. p之後的部分會被當作Promise then 微任務

進入實戰

console.log('script start')

async function async1() {
    await async2()
    console.log('async1 end')
    console.log('我跟async1一起喔')
}
async function async2() {
    console.log('async2 end') 
}
async1()

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

new Promise(resolve => {
    console.log('Promise')
    resolve()
})
    .then(function() {
    console.log('promise1')
})
    .then(function() {
    console.log('promise2')
})

console.log('script end')

分析

這裡Chrome老版本跟新版本會有差,但在此我們僅分析最新版本

  • 完成同步部分:

    1. 首先打印 script start,接下來定義了兩個函數,並執行了async1
    2. 觀看async1函數長怎樣
        async function async1() {
            await async2() // 這一行相對於執行了resolve(async2())注意這個是同步所以會優先打印
            // 底下整個會被放進微任務對列裡面
            console.log('async1 end') 
            console.log('我跟async1一起喔')
        }
    3. 打印完async2 end,繼續執行看到setTimeout將它新增至宏任務隊列裡
    4. 看到new Promise分析一下
        new Promise(resolve => {
            console.log('Promise') // 這裡發生的是同步的(需要馬上請求數據)
            resolve()
        })
    5. 打印完Promise繼續執行,看到第一個then,放進微任務隊列裡面(注意只有第一個,因為第二個then需要等第一個執行完)
    6. 打印script end
    

    https://ithelp.ithome.com.tw/upload/images/20200921/20124350zKMwKIfyTl.png

    目前的宏任務隊列以及微任務

    https://ithelp.ithome.com.tw/upload/images/20200921/20124350naw4INQPCp.png

  • 完成異步

    1. 優先執行微任務,且按照隊列先進先出的邏輯當然是優先處理async1 await以下的部分,所以打印出
       async1 end 跟 我跟async1一起喔
    2. 再來執行promise1的部分,打印出promise1,接下來發現之下竟然還有then,因此將promise2放到微任務隊列
    

    https://ithelp.ithome.com.tw/upload/images/20200921/20124350QcrEpElbJp.png

    https://ithelp.ithome.com.tw/upload/images/20200921/20124350X0R2XgtoDU.png

    3. 做到這裡,發現微任務隊列還有東西,因此繼續處理微任務,打印出promise2
    4. 剩下一個宏任務setTimeout,因此打印出setTimeout
    

    https://ithelp.ithome.com.tw/upload/images/20200921/20124350o0PMQvbuSo.png


上一篇
Day 10 [其他02] [譯] 理解JavaScript中的執行上下文和執行棧
下一篇
Day 12 [異步 01] JavaScript異步編程的4種方法
系列文
從技術文章深入學習 JavaScript29
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言