带你一步了解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. 优化性能:事件循环机制能够根据任务的优先级和顺序,合理地安排任务的执行顺序,从而提供更好的性能。例如,事件循环中微任务队列可以优先执行,确保及时响应用户操作,提高程序的流畅度。

宏任务

常见的异步操作:

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

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

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

  • 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的解释里是把任务分类,同类型的任务放在相同的队列里,浏览器根据队列的优先级进行获取执行,例如有定时队列、微队列等等,微队列的优先级最高。

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

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

相关推荐
曹牧31 分钟前
C#:空合并运算符
java·前端·数据库
小羊没烦恼!1 小时前
铁道部新客票系统设计(二)
开发语言·前端·数据库·python·oracle·c#·php
智能直播4 小时前
RK3518:1.5G 内存 + 8G 存储,如何跑起硬件编解码、Web、AI 识别、绿幕特效
前端·人工智能·5g
掘金酱5 小时前
【10.24 特别活动】AI 时代,程序员的新饭碗在哪里?
前端·人工智能·后端
excel5 小时前
多进程环境下使用 PM2 部署 Socket.IO:需要注意什么
前端
IT_陈寒6 小时前
Redis的原子性竟然失效了?记一次分布式锁翻车实录
前端·人工智能·后端
_菜鸟果果7 小时前
高德地图 JS API 2.0 在国产 Linux/ARM 电脑上渲染失败 的排查与解决
linux·开发语言·javascript
时间的情敌7 小时前
《回顾 2018 前端:Vue2 成熟期,Webpack4 发布,PWA 大火》
前端
Java后端的Ai之路8 小时前
前端技术Vue-09-webpack-vuejs-实战
前端·vue.js·webpack