浏览器事件循环机制(Event Loop)

287 阅读2分钟

浏览器是多线程的(否则一些异步的事件谁来处理?),浏览器的主要线程有以下几种:

  • js 引擎线程
  • 事件触发线程
  • 定时器触发线程
  • 异步 http 线程
  • GUI 渲染线程

因为 js 引擎是单线程的,所以当遇到定时器、DOM 事件监听的时候,js 引擎会交给相应的线程,等处理完毕后再把这些事件对应的回调放到 js 引擎中的任务队列中。这个任务队列就是和 Event Loop 息息相关的。

微任务和宏任务

上述的任务队列一共有两种,微任务和宏任务。

宏任务macrotask

  • setTimeout
  • setInterval
  • xhr的回调
  • dom事件监听的回调

微任务microtask

  • Promise
  • MutationObserver

Event Loop

Event Loop是一个执行机制,在浏览器和nodejs分别有不同的实现,本文均是以浏览器的Event Loop为例。
当 js 引擎执行全局script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(执行完后会分别放入相应的任务队列);

  1. 全局script代码执行完毕后,调用栈call stack会清空;
  2. 从微队列microtask queue中取出位于队首的回调任务,放入调用栈call stack中执行,执行完后microtask queue长度减 1,这样重复直到清空队列注意,如果在执行 microtask 的过程中,又产生了 microtask,那么会加入到队列的末尾,也会在这个周期被调用执行
  3. 取出宏队列macrotask queue中位于队首的任务,放入call stack中执行;
  4. 执行完毕重复操作2、3、4,直至微队列和宏队列全部清空。

来个例子

看下面一个例子的输出(网上随便找的,还蛮有典型性的):

console.log('script start')

async function async1() {
  await async2()
  console.log('async1 end')
}
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')

// 在谷歌浏览器上最后的输出:
script start
async2 end
Promise
script end
async1 end
promise1
promise2
setTimeout

重要的几点:

  1. Promise的入参是一个立即执行的方法,所以Promise会在第一次循环时输出;
  2. Promisethen方法入参如果没有返回一个promise对象,会默认返回一个fulfilled状态的promise,所以两个 then 里的回调都会放到同一个微任务队列中,最后同时输出;
  3. async、await都会转为promise