面试题答案
一键面试JavaScript 事件循环机制
- 调用栈(Call Stack):JavaScript 是单线程语言,这意味着它同一时间只能执行一个任务。调用栈就是用来记录代码执行顺序的栈结构。当函数被调用时,就会被压入调用栈,函数执行完毕后,就会从调用栈中弹出。例如:
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
function calculate() {
let result1 = add(2, 3);
let result2 = subtract(5, 1);
return result1 + result2;
}
calculate();
在执行 calculate
函数时,calculate
函数首先被压入调用栈,接着 add
函数被压入调用栈,add
函数执行完毕返回结果后从调用栈弹出,然后 subtract
函数被压入调用栈,执行完毕弹出,最后 calculate
函数执行完毕也从调用栈弹出。
- 任务队列(Task Queue):也叫消息队列,用于存放异步任务产生的回调函数。JavaScript 中的异步任务(如
setTimeout
、setInterval
、Promise
等)不会立即执行,而是将它们的回调函数放入任务队列中。例如:
setTimeout(() => {
console.log('This is a timeout callback');
}, 1000);
console.log('This is a synchronous log');
这里 setTimeout
的回调函数会被放入任务队列,而同步的 console.log
会先在调用栈中执行。
- 事件循环(Event Loop):事件循环的作用就是不断地检查调用栈是否为空,如果为空,就从任务队列中取出一个任务(回调函数)放入调用栈中执行。这个过程是循环往复的,所以叫事件循环。具体步骤如下:
- 首先执行调用栈中的同步任务。
- 同步任务执行完毕,调用栈为空,事件循环开始工作。
- 检查任务队列,如果有任务,就将队列中的第一个任务取出放入调用栈执行。
- 执行完这个任务后,调用栈再次为空,事件循环继续检查任务队列,重复上述过程。
对事件处理过程的影响
- DOM 事件监听:当页面发生 DOM 事件(如点击、滚动等)时,相应的事件处理函数会被放入任务队列。例如,为一个按钮添加点击事件:
<button id="myButton">Click me</button>
<script>
const button = document.getElementById('myButton');
button.addEventListener('click', () => {
console.log('Button clicked');
});
</script>
当按钮被点击时,点击事件的回调函数会被放入任务队列,等待调用栈为空时执行。
- 异步任务:在复杂应用中,除了 DOM 事件监听,还有很多异步任务,如网络请求(通过
fetch
等)、定时器等。这些异步任务的回调同样会被放入任务队列。例如:
fetch('https://example.com/api/data')
.then(response => response.json())
.then(data => console.log(data));
setTimeout(() => {
console.log('Timeout after 2 seconds');
}, 2000);
fetch
操作完成后,then
中的回调函数会被放入任务队列,setTimeout
的回调函数也会在指定时间后放入任务队列。
- 任务执行顺序协调:在包含大量 DOM 事件监听和异步任务的复杂应用中,事件循环按照以下规则协调任务执行顺序:
- 同步任务优先执行,直到调用栈为空。
- 任务队列中的任务按照先进先出(FIFO)的原则依次进入调用栈执行。所以如果有多个 DOM 事件回调和异步任务回调在任务队列中,先进入队列的先执行。
- 微任务(如
Promise
的then
回调)会在本轮事件循环的任务队列清空之前执行。也就是说,一旦调用栈为空,会先执行所有微任务,然后再去任务队列中取宏任务(如setTimeout
回调、DOM 事件回调等)执行。例如:
Promise.resolve().then(() => {
console.log('Microtask');
});
setTimeout(() => {
console.log('Macrotask');
}, 0);
console.log('Synchronous');
输出结果会是 Synchronous
、Microtask
、Macrotask
。因为同步任务 console.log('Synchronous')
先执行,然后 Promise.resolve().then
的微任务在任务队列清空之前执行,最后 setTimeout
的宏任务执行。
综上所述,JavaScript 的事件循环机制通过调用栈、任务队列和事件循环本身,有效地协调了同步任务、DOM 事件监听以及各种异步任务的执行顺序,确保复杂应用能够有序运行。