带你一步了解Event-loop,宏/微任务执行

写这篇文章的原因

这段时间,和前端小伙伴们聊天,一直在谈论Event-loop这个问题。不论你是javascript新手还是老鸟,不论是面试求职,还是日常开发工作,我们经常会遇到这样的情况:给定的几行代码,我们需要知道其输出内容和顺序。

为什么要有Event-loop

因为Javascript设计之初就是一门单线程语言,因此为了实现主线程的不阻塞,解决用户界面无响应,或者及时处理其他任务。Event Loop这样的方案应运而生,它解决这个问题,使得JavaScript能够高效处理异步任务

Event-loop解决了什么问题

  1. 处理耗时操作:在 Web 开发中,包括网络请求、文件读写、数据库访问等操作都是耗时的,如果按照同步方式执行,会导致界面卡顿,用户体验不佳。通过将这些操作转换为异步任务,并在异步任务完成后通知主线程执行回调,可以避免阻塞主线程,提高程序的响应性。
  2. 支持并发处理:事件循环使得可以同时处理多个任务。通过将任务交给相应的 API 处理后,主线程可以继续执行其他任务,而不必等待异步任务的完成。这样可以极大地提高程序处理并发任务的能力。
  3. 实现非阻塞 I/O:事件循环使得 JavaScript 能够高效地处理 I/O 操作,特别是在 Node.js 服务器端开发中。通过将 I/O 操作交给操作系统或底层库处理,并在 I/O 操作完成后通知主线程执行回调,可以最大限度地利用计算机资源,提高系统吞吐量。
  4. 优化性能:事件循环机制能够根据任务的优先级和顺序,合理地安排任务的执行顺序,从而提供更好的性能。例如,事件循环中微任务队列可以优先执行,确保及时响应用户操作,提高程序的流畅度。

宏任务

常见的异步操作:

  • 定时器任务(setTimeoutsetInterval):通过指定一定的时间间隔或延迟来执行回调函数。

  • UI 渲染:浏览器在重绘和渲染页面时会将相关操作作为宏任务处理。

  • 事件监听器(clickkeyup等事件):当用户执行某个交互操作时,相应的事件监听器会被触发,将其处理作为宏任务执行。

  • AJAX 请求和服务器响应:当发送 AJAX 请求并接收到服务器响应时,相关的回调函数将被执行作为宏任务。

  • Script标签:保证代码的独立执行,防止页面渲染被阻塞

微任务

  • Promise 的 .then().catch():当 Promise 对象的状态改变时,相关的回调函数将被放入微任务队列中,等待执行。

  • MutationObserver:用于监视 DOM 树的变化,并在发生变化时触发相应的回调函数

  • await(下面有扩展)

宏/微任务的执行机制

在事件循环中,每次执行一个宏任务后,会检查微任务队列是否为空。如果有微任务,则会依次执行所有的微任务,直到微任务队列为空。然后,再执行下一个宏任务。这样的机制保证了微任务的执行优先级高于宏任务。

关系图如下:

下面是一个较复杂的代码示例,用来解释宏任务和微任务的执行机制:

js 复制代码
console.log('Start');

setTimeout(function() {
  console.log('Timeout 1');
  
  Promise.resolve().then(function() {
    console.log('Promise 1');
  });
}, 0);

setTimeout(function() {
  console.log('Timeout 2');
  
  Promise.resolve().then(function() {
    console.log('Promise 2');
  });
}, 0);

Promise.resolve().then(function() {
  console.log('Promise 3');
});

console.log('End');
    
    
   // 执行上述代码的结果如下所示
   // Start 
   // End 
   // Promise 3 
   // Timeout 1 
   // Promise 1 
   // Timeout 2 
   // Promise 2

解释运行过程如下:

  1. 首先输出 Start
  2. 执行第一个 setTimeout,将定时器任务1添加到宏任务队列中。
  3. 执行第二个 setTimeout,将定时器任务2添加到宏任务队列中。
  4. 执行第一个 Promise.resolve().then(),将微任务1添加到微任务队列中。
  5. 输出 End
  6. 当前宏任务执行完毕,检查是否有微任务需要执行。
  7. 执行微任务队列中的第一个任务,输出 Promise 3
  8. 检查是否有新的宏任务需要执行,发现定时器任务1。
  9. 等待时间间隔(这里是0毫秒)过去后,定时器任务1被添加到宏任务队列。
  10. 执行宏任务队列中的第一个任务,输出 Timeout 1
  11. 执行定时器任务1内部的 Promise.resolve().then(),将微任务2添加到微任务队列中。
  12. 执行微任务队列中的第一个任务,输出 Promise 1
  13. 检查是否有新的宏任务需要执行,发现定时器任务2。
  14. 等待时间间隔(这里是0毫秒)过去后,定时器任务2被添加到宏任务队列。
  15. 执行宏任务队列中的第一个任务,输出 Timeout 2
  16. 执行定时器任务2内部的 Promise.resolve().then(),将微任务3添加到微任务队列中。
  17. 执行微任务队列中的第一个任务,输出 Promise 2

对于Vue3和react任务队列的理解

任务队列里面同时存在微任务和宏任务,是先执行微任务的,而且要把所有的微任务清空再去执行宏任务,执行宏任务之后就会去进行浏览器渲染,所以微任务在涉及到渲染任务的时候,本质还是一个同步任务,所以 React 的异步更新是一个宏任务。

比如说 Vue3 的更新是微任务,同时有很多个组件需要更新,执行了一个组件的更新任务之后,浏览器是还不会有结果,因为它是微任务,它要等所有组件的更新任务都执行完了才会去进行浏览器渲染,但 React 执行完一个更新任务之后,浏览器就会有结果了,因为 React 更新是宏任务。

微任务说实话就像打补丁一样,不应该执行过多的逻辑,Vue使用微任务因为它不像React是全量更新,更小的颗粒度意味着更小的更新任务,使用微任务足够了。

例如:你有一个 DIV1 和 DIV2,你在 JS 里面进行更新 DIV1,然后再更新 DIV2,在你更新完 DIV1的时候,浏览器肯定是还没更新 DIV1的,因为你这个时候还要执行 DIV2 的更新代码,因为 JS 的执行线程和浏览器的渲染线程是互斥的,所以你要想更新完 DIV1的时候,浏览器就要把它渲染出来,你就要在更新完 DIV1 之后,把控制权交给浏览器的渲染进程,所以你就要用宏任务去更新 DIV1,因为宏任务执行完了之后,控制权将到浏览器的渲染进程上。

扩展

Promise

理解

Promise 是 JavaScript 中用于处理异步操作的内置对象。它被广泛用于解决回调地狱(callback hell)和链式异步操作的问题,提供了一种更优雅和可读性更高的方式来处理异步代码。

Promise 对象实例状态

  • pending(进行中):初始状态,表示异步操作正在进行中,尚未成功或失败。

  • fulfilled(已成功):表示异步操作已成功完成。

  • rejected(已失败):表示异步操作已失败。

Promise 对象方法

  • then(onFulfilled, onRejected):当Promise状态变为fulfilled(解决)时,调用onFulfilled函数;当状态变为rejected(拒绝)时,调用onRejected函数。这个方法可以链式调用,返回一个新的Promise对象。

  • catch(onRejected):捕获Promise链中的任何拒绝错误,并执行相应的回调函数。它也可以被视为then(null, onRejected)的简写形式。

  • finally(onFinally):不管Promise最后是解决还是拒绝,都会执行onFinally回调函数。它返回一个新的Promise对象,允许你在Promise链中进行额外的操作。

  • Promise.resolve(value):返回一个已解决的Promise对象,其值为给定的value。如果value本身就是一个Promise对象,则直接返回这个Promise。

  • Promise.reject(reason):返回一个已拒绝的Promise对象,其原因为给定的reason。

  • Promise.all(iterable):接收一个可迭代对象(如数组),并返回一个新的Promise对象。该Promise对象在可迭代对象中所有的Promise都解决之后才会解决,并将解决结果按照顺序组成一个数组。

  • Promise.race(iterable):接收一个可迭代对象,并返回一个新的Promise对象。该Promise对象将在可迭代对象中的第一个解决或拒绝的Promise发生后立即解决或拒绝,将第一个解决或拒绝的结果作为自己的解决或拒绝结果

async/await

理解

async/await 是建立在 Promise 的基础上的,async 关键字用于修饰一个函数,使其返回一个 Promise 对象,而 await 关键字则用于等待一个 Promise 对象的解决(resolve)或拒绝(reject),并在异步操作完成后获取其结果

补充

实际上最新的规范里已经删掉宏任务的概念了,W3C的解释里是把任务分类,同类型的任务放在相同的队列里,浏览器根据队列的优先级进行获取执行,例如有定时队列、微队列等等,微队列的优先级最高。

至此,文章就分享完毕了。

文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞和关注😊

相关推荐
吃杠碰小鸡11 分钟前
commitlint校验git提交信息
前端
虾球xz42 分钟前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇1 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒1 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员1 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐1 小时前
前端图像处理(一)
前端
程序猿阿伟1 小时前
《智能指针频繁创建销毁:程序性能的“隐形杀手”》
java·开发语言·前端
疯狂的沙粒1 小时前
对 TypeScript 中函数如何更好的理解及使用?与 JavaScript 函数有哪些区别?
前端·javascript·typescript
瑞雨溪1 小时前
AJAX的基本使用
前端·javascript·ajax
力透键背1 小时前
display: none和visibility: hidden的区别
开发语言·前端·javascript