阅读 29

JS:事件循环机制(Event Loop)

做道题

相信许多前端开发在面试中都遇到过关于代码执行顺序的问题,这里我们不妨先回顾一下,思考下面代码的输出结果。

setTimeout(() => {
  console.log(1)
}, 0)

new Promise((resolve, reject) => {
  console.log(2)
  resolve(3)
}).then(val => {
  console.log(val)
})

console.log(4)
复制代码

答案是

2
4
3
1
复制代码

看了这个结果,在我们还未理解 Event Loop 之前可能会有这样的疑问,为什么给 setTimeout 传入的是 0 毫秒,回调函数却是最后执行的?

这其实是因为在 JS 中以 0 为第二参数的 setTimeout 并不表示在 0 毫秒后就立即执行回调函数,而是取决于 任务队列(Task Queue) 里待处理的任务数量。因此第二个参数仅仅表示最少延迟时间,而非确切的等待时间。fn-event-loop

另外,HTML5 标准规定了 setTimeout 方法的第二个参数的最小值不得低于 4 毫秒(在此之前,老版本的浏览器都将最短间隔设为 10 毫秒),如果低于这个值,就会自动增加。fn-set-timeout

那么什么是任务队列

单线程 是 JavaScript 核心特征之一。这意味着,在 JS 中所有任务都需要排队执行,前一个任务结束,才会执行后一个任务。

但这就造成了如果前一个任务耗时很长,后一个任务就不得不一直等待。比如我们向服务器请求一段数据,由于网络问题,可能需要等待 60 秒左右才能成功返回数据,此时只能等待请求完成,JS 才能去处理后面的代码。

这显然是不太合理的。

于是,JavaScript 将所有任务分成了同步任务异步任务

  • 同步任务(Synchronous)

    同步任务指的是当前一个(如果有)任务执行完毕,接下来可以立即执行的任务。这些任务将在主线程上依次排队执行。

    下面的 for(){}console.log() 将会依次执行,最终输出 0 1 2 3 4 done

    for (let i = 0; i < 5; i++) {
      console.log(i)
    }
    console.log('done')
    复制代码
  • 异步任务(Asynchronous)

    异步任务相对于同步任务,指的是不需要进入主线程排队执行,而是进入任务队列的任务。当被通知可以执行的时候,该任务才会进入主线程执行。

    下面的 then() 方法需要等待 Promiseresolve() 之后才能执行,它是一个异步任务。最终输出 1 3 2

    console.log(1)
    
    Promise.resolve().then(() => {
      console.log(2)
    })
    
    console.log(3)
    复制代码

具体来说就是,所有同步任务会在主线程上依次排队执行,形成一个执行栈(Execution Context Stack)。主线程之外,还存在一个任务队列。当异步任务有了运行结果,会在任务队列之中放置对应的事件。当执行栈中的所有同步任务执行完毕,任务队列里的异步任务就会进入执行栈,然后继续依次执行。fn-runyifeng

整个过程是循环不断的,所以这种运行机制被称为 Event Loop(事件循环)。

宏任务和微任务

在 JavaScript 中,任务通常也被分为宏任务微任务

  • 宏任务(MacroTask)

    • script(整体脚本)

    • Timer(setTimeout、setInterval)

    • UI render

    • I/O

    • postMessage

    • MessageChannel

    • setImmediate(Node.js 环境)

  • 微任务(MicroTask)

    • Promise

    • MutaionObserver

    • process.nextTick(Node.js 环境)

在 Event Loop 事件循环中,每一次循环(tick)的任务如下:

  • 执行栈选择最先进入队列的宏任务执行

  • 检查是否存在微任务,如果存在则不停的执行,直至清空

  • 浏览器更新渲染

测试题 · 说出下面代码的执行结果

  • 入门

    setTimeout(() => {
      console.log(1)
    }, 0)
    for (let i = 2; i <= 3; i++) {
      console.log(i)
    }
    console.log(4)
    setTimeout(() => {
      console.log(5)
    }, 0)
    for (let i = 6; i <= 7; i++) {
      console.log(i)
    }
    console.log(8)
    复制代码
  • 进阶

    console.log(1)
    
    async function async1() {
      await async2()
      console.log(2)
    }
    async function async2() {
      console.log(3)
    }
    async1()
    
    setTimeout(() => {
      console.log(4)
    }, 0)
    
    new Promise(resolve => {
      console.log(5)
      resolve()
    })
      .then(() => {
        console.log(6)
      })
      .then(() => {
        console.log(7)
      })
    
    console.log(8)
    复制代码
  • 高手

    console.log(1)
    
    function a() {
      return new Promise(resolve => {
        console.log(2)
        setTimeout(() => {
          console.log(3)
        }, 0)
        resolve()
      })
    }
    
    a().then(() => {
      console.log(4)
    })
    复制代码
文章分类
前端
文章标签