js的事件循环机制
面试高频问题之一。JavaScript是单线程的,意味着他一次只能执行一个任务。为了处理异步操作(如网络请求、定时器等),他依赖于循环机制。事件循环允许javascript执行非阻塞代码,使得如网络请求、文件读写等异步操作能够不被阻塞主线程。
事件循环的机制
1.调用栈(call stack)
事件循环的核心是基于调用栈(Call Stack)的。调用栈是一种数据结构,用于存储代码执行时调用的函数。当一个函数被调用时,它会被推入调用栈中,执行完毕后,再从调用栈中弹出。
2.任务队列(Task Queue/Event Queue)
当异步操作(如setTimeout、Promise、AJAX请求等)完成时,相应的回调函数或解决(resolve)值会被放入任务队列中。任务队列遵循FIFO(先进先出)的原则。
3.事件循环(Event Loop)
事件循环是不断运行的过程,他监视调用栈和任务队列。如果调用栈为空,事件循环会从任务队列中取出一个任务并放入调用栈中执行。这个过程会不断重复,直到任务队列为空。
4.微任务队列(Miscrotask Queue)
在ES6中引入了Promise,与之相关的异步操作完成后会将回调函数放入微任务队列中。微任务队列的优先级高于任务队列。当调用栈为空时,事件循环首先会清空微任务队列中的所有任务,然后再去检查任务队列。
事件循环的工作流程
1.执行全局代码:全局代码被推入调用栈执行。
2.遇到异步操作:如果遇到异步操作(如setTimeout、Promise等),相应的回调或解决值会被放入任务队列或微任务队列。
3.调用栈为空:当调用栈为空时,事件循环开始工作,直到微任务队列为空。
4.处理微任务:首先检查微任务队列,如果有任务则依次执行,直到微任务队列为空。
5.处理宏任务:然后再检查任务队列,如果有任务则取出一个任务放入调用栈执行。
6.重复以上步骤:重复步骤4和5,直到所有任务都被处理完毕。
示例代码
console.log('Start');
setTimeout(() => {
console.log('Timeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise');
});
console.log('End');
//输出顺序:Start =》 End =》Promise =》 Timeout
关键点
- 调用栈:同步代码立即执行。
- 微任务队列:微任务优先于宏任务。
- 任务队列:setTimeout回调在微任务处理完毕后执行。
宏任务
宏任务是由浏览器或JavaScript引擎提供的异步任务,通常包括以下操作:
setTimeout和setInterval:定时器回调。I/O操作:如文件读写、网络请求等。DOM事件回调:如点击事件、键盘事件等。requestAnimationFrame:浏览器渲染前的回调。script标签中的代码:整个脚本的执行也是一个宏任务。
特点
- 宏任务会被放入任务队列中。
- 每次事件循环中,只会执行一个宏任务。
- 宏任务的执行优先级低于微任务。
微任务
微任务是指优先级更高的异步任务,通常包括以下操作:
Promise的回调:如then、catch、finally。MutationObserver:监听DOM变化的回调。queueMicrotask:显式添加微任务的api。
特点
- 微任务会被放入微任务队列中。
- 每次事件循环中,会清空整个微任务队列(即执行所有微任务)。
- 微任务的优先级高于宏任务。
注意事项
- 避免微任务嵌套:如果在微任务中继续添加微任务,会导致微任务队列无法清空,从而阻塞事件循环。
- 宏任务的延迟:
setTimeout(fn, 0)并不保证立即执行,因为需要等待当前微任务队列清空。 await会使async函数暂停,直至对应的Promise状态变为已解决。
一些题目
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
//输出 5个5;`var` 声明的变量没有块级作用域,`setTimeout` 是异步任务,会在 `for` 循环结束后才执行。此时 `i` 的值已经变为 5,所以每个 `setTimeout` 中的回调函数打印的都是 5。
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
//输出0、1、2、3、4;let是块级作用域;每个 `setTimeout` 都会捕获到当前循环的 `i` 值。
function asyncTask1() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('Async Task 1 completed');
resolve();
}, 2000);
});
}
function asyncTask2() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('Async Task 2 completed');
resolve();
}, 1000);
});
}
async function main() {
console.log('Main function started');
await asyncTask1();
console.log('After Async Task 1');
const task2Promise = asyncTask2();
console.log('Async Task 2 is in progress');
await task2Promise;
console.log('After Async Task 2');
}
console.log('Before calling main function');
main();
console.log('After calling main function');
执行顺序分析
- 同步代码优先执行:在调用main函数之前先执行
console.log('Before calling main function'); - 调用main函数:内部的
console.log('Main function started');同步代码立即执行。 - 执行
await asyncTask1():asyncTask1函数返回一个Promise,由于使用了await,main函数会暂停执行,等待asyncTask1的Promise被解决(resolve)。asyncTask1内部有一个setTimeout,它会在 2 秒后执行回调函数。在这 2 秒内,JavaScript 引擎会继续执行后续的同步代码。 - 继续执行同步代码:执行
console.log('After calling main function');。 asyncTask1的Promise被解决:2 秒后,asyncTask1中的setTimeout回调函数执行,输出Async Task 1 completed,asyncTask1的Promise被解决,main函数继续执行,输出After Async Task 1。- 执行
asyncTask2:调用asyncTask2函数,它返回一个Promise。asyncTask2内部也有一个setTimeout,会在 1 秒后执行回调函数。接着输出Async Task 2 is in progress。然后main函数再次暂停,等待asyncTask2的Promise被解决。 asyncTask2的Promise被解决:1 秒后,asyncTask2中的setTimeout回调函数执行,输出Async Task 2 completed,asyncTask2的Promise被解决,main函数继续执行,输出After Async Task 2。
输出结果
Before calling main function
Main function started
After calling main function
Async Task 1 completed
After Async Task 1
Async Task 2 is in progress
Async Task 2 completed
After Async Task 2
练习
function delayLog(message, time) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(message);
resolve();
}, time);
});
}
function simpleSyncTask() {
console.log('This is a simple sync task');
}
async function asyncFlow() {
console.log('Async flow starts');
await delayLog('Delayed log 1 after 1.5 seconds', 1500);
const secondPromise = delayLog('Delayed log 2 after 1 second', 1000);
simpleSyncTask();
await secondPromise;
console.log('Async flow ends');
}
console.log('Before starting async flow');
asyncFlow();
console.log('After starting async flow');
输出结果
Before starting async flow
Async flow starts
After starting async flow
1.5s后
Delayed log 1 after 1.5 seconds
This is a simple sync task
1s后
Delayed log 2 after 1 second
Async flow ends