Event loop, macrotask and microtask

前言
2020 秋天,我将用 30 天的时间,来尝试回答和网路前端开发相关的 30 个问题。30 天无法一网打尽浩瀚的前端知识,有些问题可能对有些读者来说相对简单,不过期待这趟旅程,能帮助自己、也帮助读者打开不同的知识大门。有兴趣的话,跟着我一起探索吧!

JavaScript 的运作环境是 single thread,也就是说,在同一个时间点,只能处理一件事情。

如果有一个 A 任务需要一些时间才能被完成,那麽执行到 A 任务的时候,整个 JavaScript 程序就会停在那边,等到 A 任务完成之後,才能继续往下执行。但在等待的过程中,无法处理其他任务,以及处理使用者的互动,因此,在 JavaScript 采用了非同步的方式来处理。

以下面的程序码来解释非同步:

console.log('Hello world!')

setTimeout(()=>{
    console.log('I am back')
}, 10000)

console.log('Hello world again!')

首先,JavaScript 会逐行执行程序码,所以会先在 console 看到 Hello world 的字样。

接着,会执行到 setTimeout function。setTimeout 是非同步的 function,负责等待一段时间之後,在执行当中的 callback function。因此这里 JavaScript 会做的事情是,执行 setTimeout 然後将等待的任务交给浏览器来处理。之後 JavaScript 就不管了,继续执行下面的程序码,在 console 当中印出 Hello world again! 的字样

当浏览器等待完 10 秒之後,就会把 callback function 丢回到 task queue 当中,等待适当时机,再被 JavaScript 执行,印出 I am back 的字样。

想理解更多 JavaSrcipt 关於非同步的处理,可以参考 前端三十|11. [JS] 如何处理非同步事件? 这篇文章。

想更清楚知道 event loop 的运作,可以参考 What the heck is the event loop anyway? | Philip Roberts | JSConf EU 这个演讲。

When to "call back"?

但是,JavaScript 要哪时候处理那些 callback functions 呢?

有一种想法是,当非同步的 functions 执行完毕後回传的 callback function,就直接放进 call stack 里面。

这样也许看起来合理,也符合最後看到的现象。不过大部分的程序不会只有这样短短三行的程序码,也许有上百行、上千行的程序码,那麽真的要把 callback 丢回来的话,也是要等那上千行的程序码执行完毕之後,才能执行 callback。

为了要能够更精准的执行 callback functions,因此在 JavaScript 的运作环境当中,多了一个 "microtask queue" 的设计,来专门处理 callback functions 的运作。

相对於 microtask,可以想像原本的程序码任务都是 "macrotask",也就是一行一行的执行下去。当非同步任务完成,需要执行 callback function 的时,这时就会把 callback function 丢进 microtask queue,等到当前的 macrotask 执行完毕之後,一口气把 microtask queue 当中的任务清空,之後才会再执行下一个 macrotask。

所以完整的 event loop 会是:

  1. 从 macrotask queue 当中取出最旧的任务,并执行
  2. 检查 microtask queue,并执行当中所有的任务,直到清空为止
  3. 进行画面渲染(有需要的话)
  4. 检查 macrotask queue 当中是否有下一个任务需要处理。若有,回到第一步 。若无,则等待。

WeB API 也提供了 queueMicrotask 的方法,能够将 functions 直接放入 microtask queue。

另一方面,setTimeout 本身丢回来的 callback function 是一个 macrotaks,也就是说,我们透过 setTimeout 来规划一个未来一定时间後会出现的 macrotasks

Example

让我们直接来看这个 case,执行之後结果会是什麽呢?

console.log('Hello world')

setTimeout(()=>{
  console.log('start of setTimout')
  queueMicrotask(()=>{
    console.log('microtask in setTimout')
  })
  console.log('end of setTimeout')
},0)

Promise.resolve().then(()=> console.log('promise'))

queueMicrotask(()=>{
  console.log('start of microtask')
  queueMicrotask(()=>{
    console.log('microtask in microtask')
  })
  console.log('end of microtask')
})

console.log('Hello world again!')

答案是

Hello world
Hello world again!
promise
start of microtaks
end of microtaks
microtask in microtask
start of setTimout
end of setTimeout
microtask in setTimout
  1. 首先,会先执行第一行的 console.log,印出 "Hello world"
  2. 接着遇到 setTimeout,这时候 JavaScript 就会把这个任务交给浏览器做计时,然後继续往下执行
  3. 执行 Promise,而由於实际上没有等待任何执行,所以会直接执行 .then,这时候会把 .then 当中需要被执行的任务放入 "microtask queue"
  4. 执行 queueMicrotask,也就是把当中所有的东西都丢入 "microtask queue"。但是,这时候当中的另外一个 queueMicrotask 还没有被执行喔
  5. 执行 console.log 印出 "Hello world again!"
  6. 原本程序码当中该当下被执行的内容都执行完了,这时候本来应该要来跑下一个 macrotask,也就是 setTimeout 当中的 callback function,不过不要忘记,microtask queue 当中有任务需要处理,因此需要清空 microtasks 才能执行下一个 macrotask
  7. microtask queue 当中第一个出现的是 console.log('promise')
  8. 接着,出现了
      console.log('start of microtask')
      queueMicrotask(()=>{
          console.log('microtask in microtask')
          })
      console.log('end of microtask')
    
    这时候会先印出 start of microtask,接着把 console.log('microtask in microtask') 丢进 "microtask queue" 当中,最後印出 start of microtask
  9. 结束了吗?还没,因为 "microtask queue" 当中还有一个刚刚才丢进去的 console.log('microtask in microtask'),所以会印出 microtask in microtask
  10. 最後,确认 "microtask queue" 当中都没有东西之後,就来执行下一个 macrotask,也就是
      console.log('start of setTimout')
      queueMicrotask(()=>{
          console.log('microtask in setTimout')
      })
      console.log('end of setTimeout')
    
    我想到这里,应该就对执行顺序没有问题了!

End

透过 microtask queue 的方式,让 callback function 有机会在 macrotask 一连串的执行之间,在最短的时间插队并执行,完成非同步的任务。如果之後写 JavaScript 还不清楚执行顺序的话,记得回头看看非同步、event loop、microtask queue 的介绍喔 :)

Ref


TD
Be curious as astronomer, think as physicist, hack as engineer, fight as baseball player

More about me

"Life is like riding a bicycle. To keep your balance, you must keep moving."


<<:  DAY27-如何与人协同工作与好好沟通-实习生篇

>>:  Lektion 29. 反身动词・反身代名词 Reflexiv Verb und Reflexivpronomen

Day21:今天来聊一下Firewall的Evasion

最後倒数10天真的是什麽状况都有老婆下雨骑车雷铲,原定在家写的 实做LAB文章只能在医院用手机以注音...

虎你快乐啦!自己的新年图自己做 (React+Fabric.js) -上

老妹每年都会帮妈妈画春联, 今年人在国外,拍胸脯保证说会画电子档给她! 呕心沥血画了两天後,妈很满意...

爬虫怎麽爬 从零开始的爬虫自学 DAY22 python网路爬虫开爬-4翻页继续爬

前言 各位早安,书接上回我们学会透过发送 cookie 来绕过18岁守门员,今天我们要学习如何翻页继...

Day11 职训(机器学习与资料分析工程师培训班): Python程序设计, 建立Model+载入

上午:Python程序设计 早上学习function, *args, *kwargs, 全域变数 &...

Day27 [实作] 一对一视讯通话(7): 使用 Docker 封装

首先我们需要有 Docker 环境,如果还没有可以参考 Docker 安装 制作 Dockerfil...