返回

从另一个角度重新认识事件循环

前端

相信大家都已经阅读过许多关于浏览器事件循环的文章,其中多数文章通常只介绍了宏任务和微任务的执行顺序。例如,以下代码片段经常被用作面试题,它考察了宏任务和微任务的执行顺序:

console.log('start');
setTimeout(() => {
  console.log('timeout1');
  Promise.resolve().then(() => {
    console.log('promise1');
  });
});
new Promise((resolve) => {
  console.log('promise2');
  resolve();
}).then(() => {
  console.log('promise3');
});
console.log('end');

输出结果为:

start
promise2
end
promise3
timeout1
promise1

但仅仅了解宏任务和微任务的执行顺序还远远不够。事件循环涉及到的核心概念还有很多,本文将深入剖析浏览器中的事件处理机制,帮助你更透彻地理解其运作方式。

1. 执行上下文和任务队列

首先,我们需要了解执行上下文和任务队列的概念。执行上下文是 JavaScript 代码执行的环境,其中包括变量对象、函数对象和this对象。当代码在执行时,它会创建一个新的执行上下文,该执行上下文会压入执行上下文栈中。

任务队列是一个存储待执行的任务的队列。当一个事件发生时,浏览器会将对应的任务添加到任务队列中。任务队列中的任务会被按照先进先出的原则执行。

2. 宏任务和微任务

宏任务和微任务都是任务队列中的任务,但它们之间存在着一些关键的区别。宏任务是指那些需要较长时间才能执行的任务,例如 setTimeout、setInterval 和 I/O 操作。微任务是指那些不需要花费太多时间就能执行的任务,例如 Promise.resolve()、MutationObserver 和 DOM 事件。

宏任务和微任务的执行顺序是不同的。宏任务会按照先进先出的原则执行,而微任务会在宏任务执行之前执行。也就是说,当一个宏任务正在执行时,浏览器会先将所有待执行的微任务执行完毕,然后再执行下一个宏任务。

3. 事件循环

事件循环是一个不断循环的过程,它会从任务队列中取出任务并执行它们。事件循环会一直运行,直到任务队列中没有更多任务需要执行。

事件循环的流程大致如下:

  1. 检查执行上下文栈中是否有执行完毕的执行上下文。如果有,则将其弹出。
  2. 检查任务队列中是否有待执行的宏任务。如果有,则将其取出并执行。
  3. 检查任务队列中是否有待执行的微任务。如果有,则将其取出并执行。
  4. 如果执行上下文栈中还有执行中的执行上下文,则继续执行该执行上下文中的代码。
  5. 重复步骤 1-4,直到任务队列中没有更多任务需要执行。

4. 对 JavaScript 执行的影响

事件循环的运作方式对 JavaScript 的执行有着重要的影响。首先,它决定了代码执行的顺序。其次,它决定了变量的访问顺序。第三,它决定了事件的处理顺序。

代码执行顺序

事件循环决定了代码执行的顺序。宏任务按照先进先出的原则执行,而微任务会在宏任务执行之前执行。这意味着,如果在一个宏任务中调用了一个微任务,那么该微任务会在该宏任务执行完毕之前执行。

变量访问顺序

事件循环也决定了变量的访问顺序。变量的访问顺序与代码执行的顺序是一致的。这意味着,如果在一个宏任务中修改了一个变量,那么在该宏任务执行完毕之前,其他宏任务和微任务都不能访问该变量的修改值。

事件处理顺序

事件循环也决定了事件的处理顺序。当一个事件发生时,浏览器会将对应的任务添加到任务队列中。如果该事件是一个宏任务,那么它会在当前正在执行的宏任务执行完毕之后执行。如果该事件是一个微任务,那么它会在当前正在执行的宏任务执行完毕之前执行。

5. 总结

事件循环是浏览器中一个非常重要的机制,它决定了 JavaScript 代码的执行顺序、变量的访问顺序和事件的处理顺序。理解事件循环的运作方式对于编写高质量的 JavaScript 程序非常重要。

通过重新审视浏览器中的事件循环机制,我们能够更加深入地理解宏任务、微任务的执行顺序及其对 JavaScript 执行的影响。希望本文能够帮助你从不同的视角看待事件循环,并进一步提升你的 JavaScript 编程能力。