MST

星途 面试题库

面试题:JavaScript 异步编程 - 事件循环与微任务队列的深度剖析

请深入阐述 JavaScript 事件循环机制以及微任务队列的工作原理。假设在一段代码中有多个 Promise、setTimeout 以及 process.nextTick(Node.js 环境)等异步操作混合使用,详细分析它们在事件循环中的执行顺序,并说明原因。同时,谈谈如何利用这些机制来优化复杂异步应用的性能。
17.4万 热度难度
编程语言JavaScript

知识考点

AI 面试

面试题答案

一键面试

JavaScript 事件循环机制

  1. 概述:JavaScript 是单线程语言,事件循环机制让它能在非阻塞的情况下处理异步操作。它通过一个不断运行的循环,从任务队列中取出任务并执行。
  2. 运行过程:JavaScript 运行环境中有调用栈(call stack)、任务队列(task queue)和事件循环(event loop)。当执行同步代码时,函数依次进入调用栈执行,执行完出栈。遇到异步操作(如 setTimeout),不会阻塞调用栈,而是被放到任务队列中。事件循环不断检查调用栈是否为空,为空时从任务队列中取出任务放入调用栈执行,如此循环。

微任务队列工作原理

  1. 微任务与宏任务:任务队列分为宏任务队列和微任务队列。常见宏任务有 setTimeoutsetIntervalsetImmediate(Node.js)等;常见微任务有 Promise.thenprocess.nextTick(Node.js)等。
  2. 微任务执行时机:事件循环每次从宏任务队列取出一个宏任务执行完后,会先清空微任务队列中的所有微任务,再去宏任务队列取下一个宏任务。这确保了微任务在当前宏任务结束后,下一个宏任务开始前执行,能以更高优先级处理一些异步操作。

混合异步操作执行顺序分析

  1. PromisePromise 的构造函数同步执行,then 回调会被放入微任务队列。例如:
console.log('start');
const promise = new Promise((resolve) => {
  console.log('promise constructor');
  resolve();
});
promise.then(() => console.log('promise then'));
console.log('end');
// 输出:start -> promise constructor -> end -> promise then
  1. setTimeoutsetTimeout 回调会被放入宏任务队列,且有延迟执行时间(实际延迟时间可能因队列情况而不准确)。例如:
console.log('start');
setTimeout(() => console.log('setTimeout'), 0);
console.log('end');
// 输出:start -> end -> setTimeout
  1. process.nextTick(Node.js 环境)process.nextTick 回调会被放入微任务队列,且它的优先级高于 Promise.then,即每次事件循环优先处理 process.nextTick 微任务队列,再处理 Promise.then 微任务队列。例如:
console.log('start');
process.nextTick(() => console.log('process.nextTick'));
Promise.resolve().then(() => console.log('promise then'));
console.log('end');
// 输出:start -> end -> process.nextTick -> promise then

综合示例:

console.log('start');
setTimeout(() => console.log('setTimeout1'), 0);
Promise.resolve().then(() => console.log('promise then1'));
process.nextTick(() => console.log('process.nextTick1'));
setTimeout(() => console.log('setTimeout2'), 0);
Promise.resolve().then(() => console.log('promise then2'));
process.nextTick(() => console.log('process.nextTick2'));
console.log('end');
// 输出:start -> end -> process.nextTick1 -> process.nextTick2 -> promise then1 -> promise then2 -> setTimeout1 -> setTimeout2

原因是先执行同步代码,然后事件循环先处理 process.nextTick 微任务队列,再处理 Promise.then 微任务队列,最后处理宏任务队列中的 setTimeout 回调。

优化复杂异步应用性能

  1. 合理使用微任务和宏任务:对于一些需要及时响应的操作(如 DOM 渲染后的操作),可使用微任务;对于一些不需要立即执行且希望不阻塞主线程的任务(如数据的延迟处理),使用宏任务。
  2. 避免微任务过多:过多微任务可能导致事件循环长时间停留在微任务处理阶段,阻塞宏任务执行,影响页面交互等。
  3. 利用 async/await:它基于 Promise,使异步代码以同步方式书写,提高代码可读性和可维护性,同时更好地控制异步操作顺序。
  4. 控制并发数量:对于多个异步操作,合理控制并发数量,避免同时发起过多请求造成性能问题。例如,可使用 Promise.allSettled 结合限制并发数量的逻辑来处理多个异步任务。