JavaScript事件循环机制

JavaScript 的执行模型基于事件循环,其中任务分为两类:宏任务(Macro Task)和微任务(Micro Task)。理解这两者的区别和执行顺序对于掌握 JavaScript 的异步编程非常重要。

宏任务(Macro Task)

宏任务包括以下几种常见的操作:

  • setTimeout
  • setInterval
  • setImmediate(Node.js 环境)
  • I/O 操作
  • UI 渲染

微任务(Micro Task)

微任务包括以下几种常见的操作:

  • Promise 的回调函数(then, catch, finally
  • MutationObserver
  • queueMicrotask

执行顺序

  1. 同步代码:首先执行所有的同步代码,这些代码会直接进入主线程执行。
  2. 微任务队列:同步代码执行完毕后,立即执行微任务队列中的所有任务,直到微任务队列为空。
  3. 宏任务队列:微任务队列清空后,执行一个宏任务。宏任务执行完毕后,再次检查微任务队列,重复上述过程。

例子

通过一个例子来理解微任务和宏任务的执行顺序:

console.log('同步代码开始');

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

Promise.resolve().then(() => {
  console.log('微任务:Promise.then');
});

queueMicrotask(() => {
  console.log('微任务:queueMicrotask');
});

console.log('同步代码结束');

执行步骤

  1. 同步代码

    • console.log('同步代码开始'); 输出 同步代码开始
    • console.log('同步代码结束'); 输出 同步代码结束
  2. 微任务队列

    • Promise.resolve().then 回调进入微任务队列
    • queueMicrotask 回调进入微任务队列
  3. 执行微任务队列

    • queueMicrotask 回调执行,输出 微任务:queueMicrotask
    • Promise.resolve().then 回调执行,输出 微任务:Promise.then
  4. 宏任务队列

    • setTimeout 回调执行,输出 宏任务:setTimeout

最终输出顺序

同步代码开始
同步代码结束
微任务:queueMicrotask
微任务:Promise.then
宏任务:setTimeout

通过这个例子,我们可以清楚地看到同步代码、微任务和宏任务的执行顺序。

PS : setTimeout等时间相关的宏任务可能存在计时不准确的情况

setTimeout 在 JavaScript 中不准确的原因主要有以下几点:

  1. JavaScript 是单线程的:JavaScript 运行在单线程环境中,意味着它一次只能执行一个任务。如果当前有其他任务在执行(一般来说是在处理宏任务队列以及微任务队列中的任务),setTimeout 的回调函数将被推迟执行,直到主线程空闲。

  2. 事件循环机制setTimeout 的回调函数会被放入宏任务事件队列中,只有当调用栈为空时,事件循环才会从事件队列中取出回调函数执行。如果前面的任务耗时较长,setTimeout 的回调函数会被延迟执行。补充:当无同步代码执行,微任务队列为空,宏任务队列中setTimeout前的任务完成之后回调函数才会继续执行。因此可能会存在在微任务队列中存有大量的微任务时,需要等到所有微任务执行完毕之后回调函数才会继续执行的情况。

  3. 系统计时器精度:不同的浏览器和操作系统对计时器的精度有不同的实现,可能会导致 setTimeout 的实际延迟时间与预期不一致。

  4. 最小延迟时间:HTML5 规范规定,嵌套的 setTimeout 调用的最小延迟时间为 4 毫秒。这意味着即使你设置的延迟时间为 0 毫秒,实际执行时也会有至少 4 毫秒的延迟。

以下是一个简单的示例,展示 setTimeout 的不准确性:

console.log('Start');

setTimeout(() => {
    console.log('Timeout callback');
}, 1000);

const start = Date.now();
while (Date.now() - start < 2000) {
    // 模拟一个耗时操作
}

console.log('End');

在这个示例中,由于主线程被一个耗时 2 秒的操作阻塞,setTimeout 的回调函数会被延迟执行,尽管设置的延迟时间为 1 秒。

------------2025年3月11日更新------------
根据参考文章做一点更新
事件循环在过去的说法中,任务分为两个队列,一个是宏任务,一个是微任务。但是现在已经没有了宏任务的说法了。

根据 W3C 的最新解释:

每个任务都有一个任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同的队列。在一次事件循环当中,浏览器可以根据实际情况从不同的队列中取出任务执行;

浏览器必须准备好一个微任务队列,微队列中的任务优先所有其他任务执行;

相关 W3C 连接

在 Chrome 的实现中,至少包含了下面的队列:

延时队列: 用于存放计时器到达后的回调任务,优先级 中;

交互队列: 用于存放用户操作后产生的事件任务,优先级 高;

微队列: 用户存放需要最快执行的任务,优先级最高;

posted @ 2025-02-12 16:22  EmptyEmeraldTablet  阅读(28)  评论(0)    收藏  举报