【JavaScript 核心】事件循环机制

事件循环机制,微任务和宏任务的关系

本文涉及到的名词:事件循环(Event Loop),宏任务(macro-task)与微任务(micro-task),执行栈和任务队列等。

前言

JavaScript 是单线程的,同一时间只能做一件事情。如果碰到某个耗时长的任务(比如一个需要 3s 的网络请求),那么后续的任务都要等待,这种效果是无法接受的,这时我们就引入了异步任务的概念。

所以 JavaScript 执行主要包括同步任务和异步任务:

  • 同步任务:会放入到执行栈中,他们是要按顺序执行的任务;
  • 异步任务:会放入到任务队列中,这些异步任务一定要等到执行栈清空后才会执行,也就是说异步任务一定是在同步任务之后执行的。

本文所讲的 JavaScript 事件循环机制,它主要与异步任务有关。

消息队列

事件循环主要与消息队列有关,所以必须要先知道宏任务与微任务。

在任务队列中,有两种任务:宏任务和微任务。

宏任务:script 标签中的整体代码、setTimeout、setInterval、setImmediate、I/O、UI渲染
微任务:process.nextTick(Node.js)、Promise、Object.observe(不常用)、MutationObserver(Node.js)

任务优先级:process.nextTick > Promise.then > setTimeout > setImmediate

以上这些是常见的宏任务和微任务,记住就行了,不用追究为什么它是宏任务或微任务,因为就是这样规定的。

事件循环

那么什么是事件循环机制呢?

  • 一开始整个脚本(script 标签中的整体代码)作为一个宏任务执行。
  • 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列。
  • 当前宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)。
  • 当前宏任务执行完毕,开始检查渲染,然后 GUI 线程接管渲染(浏览器会在两个宏任务交接期间,对页面进行重新渲染)。
  • 渲染完毕后,JavaScript 线程继续接管,开始下一个宏任务(从任务队列中获取),依此循环,直到宏任务和微任务队列都为空。

上面这一过程就称为:事件循环(Event Loop)。

说的通俗一点:宏任务和他所产生的微任务是绑定的,一个宏任务执行完成后,开始执行这个宏任务所产生的微任务,以及微任务产生的微任务。等它们全部执行完后,才会执行下一个宏任务。

宏任务 一个事件循环结束 宏任务 宏任务

(图解 JavaScript 事件循环)

为什么要分两种任务

每一个任务的执行当中,有可能会产生新的任务,那么这些新的任务有两种插入消息队列的方式:
在这里插入图片描述

两种插入消息队列的方式

这也主要是宏任务和微任务的区别,在任务执行过程中:

  • 产生的宏任务直接插入消息队列尾部依次执行。
  • 产生的微任务直接插入当前宏任务的微任务队列中,在此宏任务执行完成后,直接执行此宏任务的微任务队列。

可以看出微任务在时效性和效率之间实现了一个有效的权衡

  • 时效性方面,微任务归根结底还是异步执行的代码,所以它一定还是比同步代码要慢。
  • 效率方面,微任务的代码纯粹就是为了异步操作,并没有任何延时需要,所以它又是异步代码中最快的。

而微任务的快体现在:它会在 DOM 重新渲染 + 宏任务执行之前,所以有这样一个结论:

  • 微任务在 DOM 渲染前触发

  • 宏任务在 DOM 渲染后触发

代码示例

我们执行如下一段代码,用上面的思路执行,看一下结果是否和预期的一致。

console.log('script start')

// 宏任务
setTimeout(() => {
  console.log('setTimeout')
}, 0)

// 微任务 跟在当前宏任务后面
new Promise((resolve) => {
  console.log('new Promise')
  resolve()
  console.log('promise body')
}).then(() => {
  console.log('promise.then 1')
}).then(() => {
  console.log('promise.then 2')
})

console.log('script end')

按照上面的思路,我们来理一下,预测一下执行结果,看看实际效果是否是这样的。

执行流程:

  • 第一次事件循环
    • 首先这一整段 JavaScript 代码作为一个宏任务先被执行
    • 遇到 console.log('script start'),打印出 "script start"
    • 遇到 setTimeout,回调函数作为宏任务压入到宏任务队列中,此时宏任务队列:[setTimeout]
    • 遇到 new Promise,由于 new 一个对象是瞬间执行的,不是异步,所以打印出 "new Promise"
    • 继续执行,由于 Promise 中的异步逻辑在 then 里面,在 then 之前的都不是异步,所以打印出 "promise body"
    • 遇到了第一个 .then,它是个微任务,将它放入微任务队列,跟在当前宏任务(整体代码)后面,此时微任务队列:[promise 1]
    • Promise 的第一个 .then 还没执行,只是排好队伍了,因此继续往后,遇到 console.log('script end'),打印出 "script end"
    • 执行第一个宏任务后的微任务
    • 执行 Promise 的第一个 .then,打印出 "promise 1",,此时微任务队列:[]
    • 又遇到 .then,它是个微任务,将它放入微任务队列,跟在当前宏任务(整体代码)后面,此时微任务队列:[promise 2]
    • 执行 Promise 的第二个 .then,打印出 "promise 2",此时微任务队列:[]
    • 整体代码执行完,微任务队列也执行完,当前的事件循环结束。
  • 第二次事件循环
    • 执行 setTimeout 的回调,打印出 "setTimeout"

预测打印结果:

"script start"
"new Promise"
"promise body"
"script end"
"promise.then 1"
"promise.then 2"
"setTimeout"

执行代码后可以发现,实际打印结果和预测一致。

复杂情况

如果遇到更复杂的场景,比如当前微任务里有微任务,微任务里有宏任务,多层嵌套的情况,只需记住一句话:微任务跟在当前宏任务后面,执行完当前宏任务,微任务就跟上,然后再执行下一个宏任务

应用场景

除了在前端面试中,会问到关于事件循环、执行栈的问题,了解 JavaScript 事件循环机制的实质作用,对我们工作中也有很大的帮助。

  • 以后我们在代码中使用 PromisesetTimeout 时,思路将更加清晰,用起来更佳得心应手。
  • 不要在 .then() 中写耗时的循环,会影响后续 DOM 渲染以及宏任务的释放。
  • 在阅读一些源码时,对于一些 setTimeout 相关的骚操作可以理解的更加深入。
  • 理解 JavaScript 中的任务执行流程,加深对异步流程的理解,少犯错误。

总结

  • JavaScript 事件循环总是从一个宏任务开始执行。
  • 一个事件循环过程中,只执行一个宏任务,但是可能执行多个微任务。
  • 执行栈中的任务产生的微任务会在当前事件循环内执行。
  • 执行栈中的任务产生的宏任务要在下一次事件循环才会执行。

最后的最后,记住,JavaScript 是一门单线程语言,异步操作都是放到事件循环队列里面,等待主执行栈来执行的,并没有专门的异步执行线程。

(完)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值