面试题答案
一键面试1. JavaScript 事件循环机制基础
JavaScript 是单线程语言,通过事件循环(Event Loop)机制来处理异步任务。事件循环的基本原理是:在执行栈(Call Stack)为空时,从任务队列(Task Queue)中取出任务放入执行栈执行。任务队列分为宏任务队列(Macro Task Queue)和微任务队列(Micro Task Queue)。常见的宏任务有 setTimeout
、setInterval
、script
(整体代码)、I/O
、UI rendering
等;常见的微任务有 Promise.then
、process.nextTick
(Node.js 环境)、MutationObserver
等。
2. setTimeout
和 setInterval
与宏微任务交互
setTimeout
和 setInterval
属于宏任务。当调用 setTimeout
或 setInterval
时,会在指定时间(最小为 4ms,受浏览器限制)后将其回调函数放入宏任务队列。当执行栈为空时,事件循环会从宏任务队列中取出一个宏任务放入执行栈执行,执行完宏任务后,会清空微任务队列,再进行下一次事件循环。
3. 代码示例及分析
console.log('start');
setTimeout(() => {
console.log('setTimeout1');
Promise.resolve().then(() => {
console.log('promise1 in setTimeout1');
});
}, 0);
Promise.resolve().then(() => {
console.log('promise1');
});
setTimeout(() => {
console.log('setTimeout2');
Promise.resolve().then(() => {
console.log('promise1 in setTimeout2');
});
}, 0);
console.log('end');
- 执行顺序分析:
- 首先执行
script
宏任务,打印start
。 - 遇到
setTimeout
,将其回调函数放入宏任务队列,由于setTimeout
延迟为 0,实际上最短延迟 4ms 左右。 - 遇到
Promise.resolve().then
,将其回调函数放入微任务队列。 - 再次遇到
setTimeout
,将其回调函数放入宏任务队列。 - 打印
end
,script
宏任务执行完毕。 - 开始清空微任务队列,执行
promise1
回调,打印promise1
。 - 第一次事件循环结束,从宏任务队列取出第一个
setTimeout
回调,打印setTimeout1
,然后Promise.resolve().then
回调放入微任务队列。 - 该宏任务执行完毕,开始清空微任务队列,打印
promise1 in setTimeout1
。 - 第二次事件循环结束,从宏任务队列取出第二个
setTimeout
回调,打印setTimeout2
,Promise.resolve().then
回调放入微任务队列。 - 该宏任务执行完毕,开始清空微任务队列,打印
promise1 in setTimeout2
。
- 首先执行
4. 复杂场景下定时器执行顺序及问题
4.1 嵌套定时器
setTimeout(() => {
console.log('outer setTimeout');
setTimeout(() => {
console.log('inner setTimeout');
}, 0);
}, 0);
执行顺序:先打印 outer setTimeout
,然后再打印 inner setTimeout
。因为外层 setTimeout
回调进入宏任务队列执行完毕后,内层 setTimeout
回调才会进入宏任务队列等待执行。
4.2 定时器与微任务复杂嵌套
setTimeout(() => {
console.log('setTimeout1');
Promise.resolve().then(() => {
console.log('promise1 in setTimeout1');
setTimeout(() => {
console.log('setTimeout2 in promise1 of setTimeout1');
}, 0);
});
}, 0);
执行顺序:
- setTimeout1
回调进入宏任务队列执行,打印 setTimeout1
。
- Promise.resolve().then
回调进入微任务队列,打印 promise1 in setTimeout1
。
- 微任务执行完毕,setTimeout2 in promise1 of setTimeout1
回调进入宏任务队列。
- 下一次事件循环执行该宏任务,打印 setTimeout2 in promise1 of setTimeout1
。
4.3 可能出现的问题
定时器不准确:由于事件循环机制,当执行栈中有长时间运行的任务时,会导致定时器回调不能按时执行。例如:
setTimeout(() => {
console.log('setTimeout callback');
}, 100);
for (let i = 0; i < 1000000000; i++) {}
这里 setTimeout
设定 100ms 后执行,但由于 for
循环长时间占用执行栈,setTimeout
回调会延迟执行。
5. 解决方案
使用 requestAnimationFrame
:适用于动画相关场景,它会在浏览器下一次重绘之前执行回调,并且会根据页面的可见性和系统资源自动调整执行频率,比 setTimeout
更精准。
function animate() {
console.log('animation frame');
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
将长任务拆分为多个微任务或宏任务:避免长时间占用执行栈。例如,将 for
循环拆分成多个部分,使用 setTimeout
或 Promise.then
来分段执行:
function longTask() {
let total = 1000000000;
let step = 1000000;
let index = 0;
function runTask() {
for (let i = 0; i < step && index < total; i++) {
index++;
}
if (index < total) {
setTimeout(runTask, 0);
}
}
runTask();
}
setTimeout(() => {
console.log('setTimeout callback');
}, 100);
longTask();
这样可以在执行 longTask
的同时,让 setTimeout
回调能相对按时执行。