Skip to content

microtask queue 与事件循环:Promise.then() 为何比 setTimeout 先执行?

理解 JavaScript 事件循环

JavaScript 是单线程的,但它通过事件循环机制实现了异步操作。理解事件循环对于掌握 JavaScript 的执行顺序至关重要,特别是当涉及到 Promise 和定时器等异步操作时。

事件循环的基本概念

javascript
console.log('1. 同步代码');

setTimeout(() => {
  console.log('3. setTimeout 回调');
}, 0);

Promise.resolve().then(() => {
  console.log('2. Promise 回调');
});

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

// 输出顺序:
// 1. 同步代码
// 1. 同步代码结束
// 2. Promise 回调
// 3. setTimeout 回调

任务队列的分类

JavaScript 的事件循环维护多个任务队列,主要包括:

  1. 宏任务队列(Macrotask Queue):包括 setTimeout、setInterval、I/O 操作等
  2. 微任务队列(Microtask Queue):包括 Promise.then、queueMicrotask、MutationObserver 等

宏任务与微任务的执行顺序

javascript
console.log('Start');

setTimeout(() => {
  console.log('setTimeout 1');
}, 0);

Promise.resolve().then(() => {
  console.log('Promise 1');
}).then(() => {
  console.log('Promise 2');
});

setTimeout(() => {
  console.log('setTimeout 2');
}, 0);

console.log('End');

// 输出顺序:
// Start
// End
// Promise 1
// Promise 2
// setTimeout 1
// setTimeout 2

微任务队列的详细分析

Promise.then 属于微任务

javascript
console.log('1. 开始');

setTimeout(() => {
  console.log('4. setTimeout');
}, 0);

Promise.resolve()
  .then(() => {
    console.log('2. Promise 第一个 then');
    return '来自第一个 then';
  })
  .then((value) => {
    console.log('3. Promise 第二个 then:', value);
  });

console.log('1. 结束');

// 输出顺序:
// 1. 开始
// 1. 结束
// 2. Promise 第一个 then
// 3. Promise 第二个 then: 来自第一个 then
// 4. setTimeout

其他微任务类型

javascript
// queueMicrotask 示例
console.log('1. 开始');

queueMicrotask(() => {
  console.log('3. queueMicrotask 回调');
});

Promise.resolve().then(() => {
  console.log('2. Promise 回调');
});

console.log('1. 结束');

// 输出顺序:
// 1. 开始
// 1. 结束
// 2. Promise 回调
// 3. queueMicrotask 回调

事件循环的执行流程

事件循环的执行遵循以下步骤:

  1. 执行所有同步代码
  2. 执行所有微任务(清空微任务队列)
  3. 执行一个宏任务(从宏任务队列取出一个任务执行)
  4. 重复步骤 2 和 3

详细的执行流程示例

javascript
console.log('Script 开始');

setTimeout(() => {
  console.log('setTimeout 1');
  Promise.resolve().then(() => {
    console.log('Promise 在 setTimeout 1 中');
  });
}, 0);

setTimeout(() => {
  console.log('setTimeout 2');
}, 0);

Promise.resolve().then(() => {
  console.log('Promise 1');
  setTimeout(() => {
    console.log('setTimeout 在 Promise 1 中');
  }, 0);
}).then(() => {
  console.log('Promise 2');
});

console.log('Script 结束');

// 输出顺序:
// Script 开始
// Script 结束
// Promise 1
// Promise 2
// setTimeout 1
// Promise 在 setTimeout 1 中
// setTimeout 2
// setTimeout 在 Promise 1 中

微任务优先级的深入理解

微任务在每次事件循环中的执行

javascript
// 演示微任务在每次宏任务后都会执行
setTimeout(() => {
  console.log('宏任务 1');
  Promise.resolve().then(() => {
    console.log('微任务 1');
  });
}, 0);

setTimeout(() => {
  console.log('宏任务 2');
  Promise.resolve().then(() => {
    console.log('微任务 2');
  });
}, 0);

// 输出顺序:
// 宏任务 1
// 微任务 1
// 宏任务 2
// 微任务 2

微任务队列的实时更新

javascript
console.log('开始');

Promise.resolve().then(() => {
  console.log('第一个微任务');
  return Promise.resolve('来自第一个微任务');
}).then((value) => {
  console.log('第二个微任务:', value);
  
  // 在微任务中添加新的微任务
  Promise.resolve().then(() => {
    console.log('第三个微任务');
  });
});

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

console.log('结束');

// 输出顺序:
// 开始
// 结束
// 第一个微任务
// 第四个微任务
// 第二个微任务: 来自第一个微任务
// 第三个微任务

实际应用场景

DOM 操作的响应

javascript
// 在 DOM 操作后立即响应
function updateDOM() {
  const element = document.getElementById('myElement');
  element.textContent = '新内容';
  
  // 使用微任务确保 DOM 更新后立即执行某些操作
  Promise.resolve().then(() => {
    console.log('DOM 已更新');
    // 执行依赖于 DOM 更新的操作
  });
}

异步错误处理

javascript
function riskyOperation() {
  // 模拟可能出错的异步操作
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() > 0.5) {
        resolve('成功');
      } else {
        reject(new Error('失败'));
      }
    }, 100);
  });
}

// 使用微任务处理错误
riskyOperation()
  .then(result => {
    console.log('操作成功:', result);
  })
  .catch(error => {
    console.error('操作失败:', error.message);
    
    // 在微任务中进行清理工作
    Promise.resolve().then(() => {
      console.log('执行清理工作');
    });
  });

常见误区和陷阱

误解执行顺序

javascript
// 常见的误解
setTimeout(() => {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(() => {
  console.log('Promise');
});

// 很多人错误地认为 setTimeout 会先执行,但实际上 Promise 会先执行

嵌套异步操作的复杂性

javascript
// 复杂的嵌套情况
setTimeout(() => {
  console.log('Outer setTimeout');
  
  Promise.resolve().then(() => {
    console.log('Inner Promise 1');
  }).then(() => {
    console.log('Inner Promise 2');
  });
  
  setTimeout(() => {
    console.log('Inner setTimeout');
  }, 0);
}, 0);

Promise.resolve().then(() => {
  console.log('Outer Promise');
});

// 输出顺序需要仔细分析事件循环

浏览器兼容性和差异

不同浏览器的实现

javascript
// 某些老旧浏览器可能有不同的实现
// 现代浏览器都遵循相同的事件循环规范

// 可以使用以下方式检测微任务支持
if (typeof Promise !== 'undefined') {
  console.log('支持 Promise 微任务');
}

if (typeof queueMicrotask !== 'undefined') {
  console.log('支持 queueMicrotask');
}

最佳实践

1. 理解执行顺序

javascript
// 始终考虑事件循环的影响
function asyncOperation() {
  console.log('1. 同步执行');
  
  setTimeout(() => {
    console.log('3. 宏任务');
  }, 0);
  
  Promise.resolve().then(() => {
    console.log('2. 微任务');
  });
  
  console.log('1. 同步执行结束');
}

2. 合理使用微任务

javascript
// 在需要立即响应时使用微任务
function immediateCallback() {
  // 执行一些同步操作
  performSyncOperation();
  
  // 使用微任务确保在当前事件循环末尾执行回调
  Promise.resolve().then(() => {
    notifyListeners();
  });
}

3. 避免阻塞微任务队列

javascript
// 避免在微任务中执行耗时操作
Promise.resolve().then(() => {
  // 不要在这里执行长时间运行的操作
  // heavyComputation(); // 这会阻塞其他微任务
  
  // 可以将耗时操作移到宏任务中
  setTimeout(() => {
    heavyComputation();
  }, 0);
});

总结

JavaScript 的事件循环机制通过宏任务和微任务队列的协调工作,实现了异步操作的有序执行。微任务(如 Promise.then)总是优先于宏任务(如 setTimeout)执行,这是因为微任务队列在每次事件循环迭代中都会被清空。理解这一机制对于编写正确的异步代码、预测执行顺序以及调试异步问题至关重要。Promise.then 比 setTimeout 先执行正是因为 Promise 属于微任务,而 setTimeout 属于宏任务,微任务具有更高的优先级。