在前端面试中,“讲讲事件循环机制”是一道超高频率的面试题,属于八股文中的经典之经典。
但现在面试已经不局限于基本概念,还需要真枪实弹的演练演练。所以本文收集了常考的几道事件循环题目,供大家查缺补漏。
先回顾下事件循环相关的知识点:
事件循环
整个事件循环大概可以分为几个步骤
- 所有任务都会在主线程上执行,形成一个执行栈
- 如果遇到异步任务,例如:
setTimeout
,执行环境将此任务放到异步队列中 - 一旦所有同步任务完成之后,就会读取任务队列,依次运行
- 只要执行栈空了之后,就会读取任务队列,不断重复这个步骤,直到所有任务完成。
宏任务与微任务
1.宏任务(Macro Task) :
- 宏任务代表的是一些较大的任务,比如 setTimeout、setInterval、I/O 操作、UI 渲染等。
- 当执行完一个宏任务后,事件循环会检查微任务队列是否为空,如果不为空,则依次执行微任务队列中的任务,直到微任务队列为空。
- 宏任务的执行顺序是按照任务的类型和其注册的顺序来确定的。
2.微任务(Micro Task) :
- 微任务是指一些比较细微的任务,比如 Promise 的回调函数、MutationObserver 的回调等。
- 微任务会在当前任务执行结束后立即执行,而不会等待其他任务执行。
- 微任务可以看作是在当前宏任务执行结束后立即执行的任务,它们在执行时机上优先于下一个宏任务。
基础题
console.log(1);
setTimeout(function () {
console.log(2);
}, 0);
Promise.resolve()
.then(function () {
console.log(3);
})
.then(function () {
console.log(4);
});
答案见:
1;
3;
4;
2;
我们一起来分析一下吧:
- 首先执行同步任务,
1
被打印出来。 setTimeout
进入宏任务队列Promise
进入微任务队列- 同步任务执行结束,查看微任务队列
- 执行微任务队列,不断提取直到队列中的微任务队列为空,此时打印
3,4
- 最后执行宏任务队列,打印
2
中级题
题目1:
console.log("begins");
setTimeout(() => {
console.log("setTimeout 1");
Promise.resolve().then(() => {
console.log("promise 1");
});
}, 0);
new Promise(function (resolve, reject) {
console.log("promise 2");
setTimeout(function () {
console.log("setTimeout 2");
resolve("resolve 1");
}, 0);
}).then((res) => {
console.log("dot then 1");
setTimeout(() => {
console.log(res);
}, 0);
});
答案如下:
"begins";
"promise 2";
"setTimeout 1";
"promise 1";
"setTimeout 2";
"dot then 1";
"resolve 1";
我们一起来分析一下吧:
- 首先执行同步任务,会先印出
'begins'
- 接着遇到
setTimeout
会把它放到宏任务列队 - 然后遇到
new Promise
会先执行,打印'promise 2'
- 然后又遇到一个
setTimeout
所以把它放到宏任务列队。 - 接着主线程又空了,所以去检查宏任务列队,执行列队中的最先的那个
setTimeout
,这时印出'setTimeout 1'
,然后遇到Promise.resolve()
,把它放到微任务列队。 - 因为宏任务每次只会执行第一个项目,所以这时会去看微任务列队,发现里面有第三步放入的
Promise.resolve()
所以印出'promise 1'
。 - 这时微任务列队空了,所以回去看宏任务列队,里面有个第二步放的
setTimeout
,所以印出'setTimeout 2'
- 然后因为这边呼叫了
resolve
所以进入到.then
于是印出'dot then 1'
- 以及再把
setTimeout
放到宏任务列队,因为这时微任务列队已经是空的,所以把宏任务列队中的setTimeout
放到执行栈,然后执行console.log(res)
,最后印出resolve 1
题目2:
console.log("begins");
setTimeout(() => {
console.log("setTimeout 1")
}, 0);
new Promise(function (resolve, reject) {
console.log("promise 2")
resolve()
}).then((res) => {
requestAnimationFrame(() => {
console.log('requestAnimationFrame 1')
requestAnimationFrame(() => {
console.log('requestAnimationFrame 2')
})
})
});
答案见:
begins
promise 2
setTimeout 1
requestAnimationFrame 1
requestAnimationFrame 2
我们一起来分析一下吧:
- 首先,会打印出
"begins"
,因为这是同步代码,会立即执行。 - 然后,会打印出
"promise 2"
,因为 Promise 的构造函数是同步执行的。 - 接着,
setTimeout
会被放入宏任务队列中,但由于设置了延迟为 0 毫秒,所以并不会立即执行,而是等待当前宏任务结束后执行。 - Promise 的
then
方法中的回调函数会被放入微任务队列中,因此会在当前宏任务执行结束后立即执行。在这个微任务中,requestAnimationFrame
会被放入宏任务队列中,但也会等待当前微任务执行结束后执行。 - 当微任务队列中的任务执行完毕后,事件循环会查看宏任务队列。由于此时宏任务队列中有
setTimeout
,因此它会被执行,控制台打印出"setTimeout 1"
。 - 接着,
requestAnimationFrame
会被执行,控制台打印出"requestAnimationFrame 1"
。 - 最后,嵌套的
requestAnimationFrame
回调函数会被执行,控制台打印出"requestAnimationFrame 2"
。
高级题
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(function () {
console.log("setTimeout");
}, 0);
async1();
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});
console.log("script end");
答案如下:
"script start";
"async1 start";
"async2";
"promise1";
"script end";
"async1 end";
"promise2";
"setTimeout";
让我们一起分析一下吧:
- 代码执行后会依顺序执行程式,所以这时会先印出
'script start'
,接着把setTimeout
把它放到宏任务列队 - 然后呼叫
async1
函式,印出'async1 start'
- 然后呼叫
await async2()
所以印出'async2'
。注意,await
后的代码会被放到微任务列队,所以不会马上印出'async1 end'
而是会把它放到微任务列队 - 接着程式继续执行,遇到
new Promise
先印出里面的'promise 1'
- 然后呼叫
resolve
,把.then
的放到微任务列队。程式继续执行,印出'script end'
- 这时候执行栈空了,所以去检查微任务列队,先印出第三步放的
'async1 end'
- 因为微任务列队会一路执行到没东西,所以继续看微任务列队,发现里面还有刚刚第四步骤放入的
resolve
代码,所以印出'promise2'
- 这时微任务列队空了,去看宏任务列队,有第一步放入宏任务列队的
setTimeout
所以把它印出