面试题答案
一键面试JavaScript 事件循环机制
- 概述:JavaScript 是单线程语言,事件循环机制让它能在非阻塞的情况下处理异步操作。它通过一个不断运行的循环,从任务队列中取出任务并执行。
- 运行过程:JavaScript 运行环境中有调用栈(call stack)、任务队列(task queue)和事件循环(event loop)。当执行同步代码时,函数依次进入调用栈执行,执行完出栈。遇到异步操作(如
setTimeout
),不会阻塞调用栈,而是被放到任务队列中。事件循环不断检查调用栈是否为空,为空时从任务队列中取出任务放入调用栈执行,如此循环。
微任务队列工作原理
- 微任务与宏任务:任务队列分为宏任务队列和微任务队列。常见宏任务有
setTimeout
、setInterval
、setImmediate
(Node.js)等;常见微任务有Promise.then
、process.nextTick
(Node.js)等。 - 微任务执行时机:事件循环每次从宏任务队列取出一个宏任务执行完后,会先清空微任务队列中的所有微任务,再去宏任务队列取下一个宏任务。这确保了微任务在当前宏任务结束后,下一个宏任务开始前执行,能以更高优先级处理一些异步操作。
混合异步操作执行顺序分析
- Promise:
Promise
的构造函数同步执行,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
- setTimeout:
setTimeout
回调会被放入宏任务队列,且有延迟执行时间(实际延迟时间可能因队列情况而不准确)。例如:
console.log('start');
setTimeout(() => console.log('setTimeout'), 0);
console.log('end');
// 输出:start -> end -> setTimeout
- 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
回调。
优化复杂异步应用性能
- 合理使用微任务和宏任务:对于一些需要及时响应的操作(如 DOM 渲染后的操作),可使用微任务;对于一些不需要立即执行且希望不阻塞主线程的任务(如数据的延迟处理),使用宏任务。
- 避免微任务过多:过多微任务可能导致事件循环长时间停留在微任务处理阶段,阻塞宏任务执行,影响页面交互等。
- 利用
async/await
:它基于Promise
,使异步代码以同步方式书写,提高代码可读性和可维护性,同时更好地控制异步操作顺序。 - 控制并发数量:对于多个异步操作,合理控制并发数量,避免同时发起过多请求造成性能问题。例如,可使用
Promise.allSettled
结合限制并发数量的逻辑来处理多个异步任务。