头图

前言

本篇文章主要讲解浏览器中事件循环(Event Loop) 那些事

单线程 JavaScript 中的同步和异步

上篇文章我们有讲到同步和异步的概念,实际上,为了实现异步模式,单线程的 JS 把所有任务都分为两种:同步任务和异步任务

同步任务是立即执行的任务,在调用栈(Call Stack)顺序执行

异步任务则不同,它在同步任务没完成之前,不会进入主线程,而是将对应回调函数注册到队列中,要理解这一步,我们先要知道任务队列

任务队列

在调用栈(Call Stack)中,如果遇到一个异步操作,那么会将对应的回调函数注册到任务队列,并且,任务队列会遵循先进先出的原则

不同的异步操作会进入到不同的任务队列中

任务队列在一贯的说法中,会细分为微任务(Micro Task)和宏任务(Macro Task),并且微任务的优先级会比宏任务高

尽管当前W3C最新标准中,已无宏任务的概念,而是用微任务队列、交互队列、延时队列等...,但目前使用微/宏任务概念来理解也并无问题

常见的微任务: Promise.thenPromise.catchMutaionObserver

常见宏任务:setTimeoutsetInterval、I/O 操作、DOM 事件、script 标签

注意,每个宏任务执行时,会先完整执行其所有同步代码,然后清空当前微任务队列(包括嵌套产生的微任务),最后才会处理下一个宏任务

图解事件循环机制

事件循环是用来处理异步任务的核心机制

用一句话来概括就是,在同步任务执行完后,回调栈会不断从任务队列中读取回调函数并压入栈中执行,这个运作流程机制就被称为事件循环(Event Loop)

  1. 所有同步代码直接进入调用栈,按顺序执行,直到调用栈清空
  2. 调用栈清空后,查找任务队列是否有任务
  3. 如果有,遵循先进先出的规则将最老的回调(最先进入任务队列的回调)推入栈中执行
  4. 重复上述流程,形成事件循环

第二、三步中,我们说会查找任务队列是否有任务并推入执行,由于微任务队列有很高的优先级,所以查找的顺序展开来说是:

  • 优先检查微任务队列,如果队列不为空,遵循先进先出的规则推入栈中执行
  • 当微任务队列清空后,当前事件循环就走完了
  • 然后从宏任务中取出一个任务(最先进入的),推入调用栈执行,就进入下一轮循环

图示

理论总是抽象的,我们来举个实际的例子,你可以先思考一下这段代码的输出顺序

    console.log('task1')

    setTimeout(()=>{
      new Promise((resolve,reject)=>{
        console.log('task2')
        resolve()
      }).then(()=>{
        console.log('task4')
      }).then(()=>{
        console.log('task7')
      })
    },0)

    new Promise((resolve,reject)=>{
      console.log('task3')
      resolve()
    }).then(()=>{
      console.log('task6')
    })

    console.log('task5')

逐步分析这段代码:

第一轮事件循环(宏任务1: script):

  1. 同步任务

    • 执行 console.log('task1'),输出 task1
    • 遇到 setTimeout ,当 time 时间结束时将其回调函数注册并放入宏任务队列
    • 执行 new Promise 中的执行器函数,输出 task3
    • 执行器函数中的 resolve 方法执行,将 then 的回调函数注册到微任务队列
    • 执行 console.log('task5'),输出 task5
    • 至此,第一轮的同步任务执行完毕
  2. 微任务队列

    • 执行 then 的回调函数,输出 task6
    • 微任务队列清空

图示:

Image

第二轮事件循环(宏任务2:setTimeout 注册的回调)

  1. 同步任务

    • 执行 setTimeout 注册的回调,创建了一个 Promise
    • 执行 Promise 执行器函数中的 console.log('task2') ,输出 task2
    • 执行 resolve 方法,将第一个 then 的回调函数注册到微任务队列
    • 至此,同步任务执行完毕
  2. 微任务队列

    • 执行第一个 then 的回调函数,输出 task4
    • then 的链式调用,注册第二个 then 的回调函数
    • 执行 第二个 then 的回调函数,输出 task7
    • 微任务队列清空

图示:

Image

最终输出顺序是:task1 task3 task5 task6 task2 task4 task7

当面对一段包含同步、异步的代码段时,能够清楚明白其运行机制,知道输出顺序,即可算掌握

总结

文章开篇我们围绕同步任务和异步任务做了介绍:

  • 同步任务按顺序,在栈中顺序执行
  • 异步任务的回调函数注册到任务队列

引出了任务队列的存在后,讲解了任务队列细分为微任务和宏任务,微任务队列的优先级最高

最后是本文核心,理解事件循环,一句话来概括就是:在同步任务执行完后,回调栈会不断从任务队列中读取回调函数并压入栈中执行

这里要注意,结合实践代码来分析才能真正理解掌握

参考资料

JavaScript 内功系列

解析 JavaScript 核心技术,提升编程内功

本文已收录至《JavaScript 内功系列》,全文地址:我的 GitHub 博客 | 掘金专栏

对你有帮助的话,欢迎 Star

交流讨论

对文章内容有任何疑问、建议,或发现有错误,欢迎交流和指正


十五
13 声望5 粉丝