参透 JavaScript —— 图解 Event Loop 事件循环

前言

本篇文章主要讲解浏览器中事件循环(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. 重复上述流程,形成事件循环

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

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

图示

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

js 复制代码
    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
    • 微任务队列清空

图示:

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

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

图示:

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

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

总结

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

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

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

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

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

参考资料

参透JavaScript系列

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

交流讨论

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

相关推荐
陌小路2 分钟前
5天 Vibe Coding 出一个在线音乐分享空间应用是什么体验
前端·aigc·vibecoding
成长ing1213810 分钟前
cocos creator 3.x shader 流光
前端·cocos creator
Alo36519 分钟前
antd 组件部分API使用方法
前端
BillKu22 分钟前
Vue3数组去重方法总结
前端·javascript·vue.js
GDAL24 分钟前
Object.freeze() 深度解析:不可变性的实现与实战指南
javascript·freeze
江城开朗的豌豆43 分钟前
Vue+JSX真香现场:告别模板语法,解锁新姿势!
前端·javascript·vue.js
这里有鱼汤1 小时前
首个支持A股的AI多智能体金融系统,来了
前端·python
袁煦丞1 小时前
5分钟搭建高颜值后台!SoybeanAdmin:cpolar内网穿透实验室第648个成功挑战
前端·程序员·远程工作
摸鱼仙人~1 小时前
Vue.js 指令系统完全指南:深入理解 v- 指令
前端·javascript·vue.js
前端进阶者1 小时前
支持TypeScript并打包为ESM/CommonJS/UMD三种格式的脚手架项目
前端