参透 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 博客 | 掘金专栏

交流讨论

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

相关推荐
赵大仁21 分钟前
Next.js 15 与 Apollo Client 的现代集成及性能优化
开发语言·javascript·性能优化
xrkhy35 分钟前
Vue之使用Vue-cli创建Webpack工程化项目
前端·vue.js·webpack
钢铁男儿38 分钟前
C#核心概念解析:析构函数、readonly与this关键字
开发语言·javascript·c#
酷爱码2 小时前
CSS3实现的账号密码输入框提示效果
前端·javascript·css3
NoneCoder2 小时前
React Context 与状态管理:用与不用
前端·react.js·面试
不爱吃饭爱吃菜2 小时前
uniapp小程序开发,判断跳转页面是否需要登录方法封装
开发语言·前端·javascript·vue.js·uni-app
霸王蟹2 小时前
React 泛型组件:用TS来打造灵活的组件。
前端·学习·react.js·typescript·前端框架
菥菥爱嘻嘻2 小时前
React---day3
javascript·react.js·ecmascript
Dontla2 小时前
React声明式编程(手动控制,大型项目,深度定制)与Vue响应式系统(自动优化,中小型项目,快速开发)区别
javascript·vue.js·react.js
阳光开朗大男孩 = ̄ω ̄=2 小时前
【JavaScript】Ajax 侠客行:axios 轻功穿梭服务器间
前端·javascript·ajax