js异步——事件循环机制

83 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 4 天,点击查看活动详情 异步一直以来都是js知识中很核心的一个部分,js的运行机制和异步解决方案是其中最重要的两个知识点涉及理解原理与实际运用。内容较多会分为两篇文章进行总结,本篇会解释异步的概念并对Event Loop事件循环机制进行总结。首先我们需要明确几点

  • JavaScript是单线程的语言
  • Event Loop是javascript的执行机制
  • 微任务、宏任务是异步任务的细分

同步、异步

JavaScript 生来作为浏览器脚本语言,主要用来处理与用户的交互、网络以及操作 DOM。 所以将JavaScript设计为单线程就是为了避免DOM 渲染结果的不可预期。 js中分为同步任务和异步任务,所有任务都可以简单的总结为两个核心过程,调用任务,得到任务结果,同步任务可以直接获得结果而异步任务则不会立即获得结果,会阻碍后续代码的执行。js在单线程的基础下通过event loop的设计核心目标为尽可能减少程序阻塞。

JS中如何实现异步

异步任务与同步任务相比无法直接获得调用结果,因此,对于同步任务和异步任务的执行机制也不同。 b708f63228b89a3eda5d4a8d8bfa71d7.png

d9c207a7f49512b1db8b7a9447ca999f.png

  • 同步任务的执行,按照代码顺序和调用顺序,支持进入调用栈中并执行,执行结束后就移除调用栈。
  • 而异步任务的执行,首先它依旧会进入调用栈中,然后发起调用,然后解释器会将其响应回调任务放入一个任务队列,紧接着调用栈会将这个任务移除。当主线程清空后,即所有同步任务结束后,解释器会读取任务队列,并依次将已完成的异步任务加入调用栈中并执行。 以上同步任务与异步任务交替执行就是通过Event Loop 机制实现的

js代码执行流程(Event Loop 机制)完整版

Event Loop本质上是一种代码调度机制

微任务、宏任务

首先要明确一点,我们整个大的 script 的执行是全局任务会被归为第一个宏任务 与字面意思相同,微任务一般相较于宏任务来说执行的更快,因此主线程代码——微任务——渲染——宏任务——微任务——渲染——宏任务....这样的执行顺序会最大程度上减少用户的等待时间 微任务的出现,使得在宏任务执行完,到浏览器渲染之前,可以在这个阶段插入任务的能力。 关于为什么要对宏任务与微任务进行分类,这篇文章中有进行讨论,juejin.cn/post/684490…

执行过程

  • 同步语句直接执行,异步语句执行时根据情况分别将回调函数加入到宏任务队列和微任务队列中
  • 全局Script代码执行完毕后,调用栈Stack会清空;
  • 从微队列microtask queue中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1;
  • 继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到直到把microtask queue中的所有任务都执行完毕。注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行;
  • microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈Stack也为空;
  • 取出宏队列macrotask queue中位于队首的任务,放入Stack中执行;
  • 执行完毕后,调用栈Stack为空;
  • 重复第3-7个步骤;
  • 重复第3-7个步骤; ......

细节补充

  • 宏队列macrotask一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务;
  • 微任务队列中所有的任务都会被依次取出来执行,知道microtask queue为空;
  • 我们前文提到过JS引擎线程和GUI渲染线程是互斥的关系,浏览器为了能够使宏任务和DOM任务有序的进行,会在一个宏任务执行结果后,在下一个宏任务执行前,GUI渲染线程开始工作,对页面进行渲染。

案例

setTimeout(() => {
    console.log(2)
    new Promise(resolve => {
        console.log(4)
        resolve()
    }).then(() => {
        console.log(5)
    })
})

new Promise(resolve => {
    console.log(7)
    resolve()
}).then(() => {
    console.log(8)
})

setTimeout(() => {
    console.log(9)
    new Promise(resolve => {
        console.log(11)
        resolve()
    }).then(() => {
        console.log(12)
    })
})
  • 同步运行的代码首先输出:1、7
  • 接着,清空microtask队列:8
  • 第一个task执行:2、4
  • 接着,清空microtask队列:5
  • 第二个task执行:9、11
  • 接着,清空microtask队列:12