你真的彻底掌握了事件循环么,实战一下才知道

421 阅读5分钟

在前端面试中,“讲讲事件循环机制”是一道超高频率的面试题,属于八股文中的经典之经典。

但现在面试已经不局限于基本概念,还需要真枪实弹的演练演练。所以本文收集了常考的几道事件循环题目,供大家查缺补漏。

先回顾下事件循环相关的知识点:

事件循环

整个事件循环大概可以分为几个步骤

  1. 所有任务都会在主线程上执行,形成一个执行栈
  2. 如果遇到异步任务,例如:setTimeout,执行环境将此任务放到异步队列中
  3. 一旦所有同步任务完成之后,就会读取任务队列,依次运行
  4. 只要执行栈空了之后,就会读取任务队列,不断重复这个步骤,直到所有任务完成。

宏任务与微任务

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";

我们一起来分析一下吧:

  1. 首先执行同步任务,会先印出 'begins'
  2. 接着遇到setTimeout 会把它放到宏任务列队
  3. 然后遇到new Promise 会先执行,打印'promise 2' 
  4. 然后又遇到一个setTimeout 所以把它放到宏任务列队。
  5. 接着主线程又空了,所以去检查宏任务列队,执行列队中的最先的那个setTimeout,这时印出'setTimeout 1' ,然后遇到Promise.resolve() ,把它放到微任务列队。
  6. 因为宏任务每次只会执行第一个项目,所以这时会去看微任务列队,发现里面有第三步放入的 Promise.resolve() 所以印出 'promise 1'
  7. 这时微任务列队空了,所以回去看宏任务列队,里面有个第二步放的 setTimeout ,所以印出 'setTimeout 2'
  8. 然后因为这边呼叫了 resolve 所以进入到 .then 于是印出 'dot then 1'
  9. 以及再把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

我们一起来分析一下吧:

  1. 首先,会打印出 "begins",因为这是同步代码,会立即执行。
  2. 然后,会打印出 "promise 2",因为 Promise 的构造函数是同步执行的。
  3. 接着,setTimeout 会被放入宏任务队列中,但由于设置了延迟为 0 毫秒,所以并不会立即执行,而是等待当前宏任务结束后执行。
  4. Promise 的 then 方法中的回调函数会被放入微任务队列中,因此会在当前宏任务执行结束后立即执行。在这个微任务中,requestAnimationFrame 会被放入宏任务队列中,但也会等待当前微任务执行结束后执行。
  5. 当微任务队列中的任务执行完毕后,事件循环会查看宏任务队列。由于此时宏任务队列中有 setTimeout,因此它会被执行,控制台打印出 "setTimeout 1"
  6. 接着,requestAnimationFrame 会被执行,控制台打印出 "requestAnimationFrame 1"
  7. 最后,嵌套的 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";

让我们一起分析一下吧:

  1. 代码执行后会依顺序执行程式,所以这时会先印出 'script start',接着把setTimeout 把它放到宏任务列队
  2. 然后呼叫 async1 函式,印出 'async1 start'
  3. 然后呼叫 await async2() 所以印出 'async2'。注意, await 后的代码会被放到微任务列队,所以不会马上印出 'async1 end' 而是会把它放到微任务列队
  4. 接着程式继续执行,遇到 new Promise 先印出里面的 'promise 1'
  5. 然后呼叫 resolve ,把 .then 的放到微任务列队。程式继续执行,印出 'script end'
  6. 这时候执行栈空了,所以去检查微任务列队,先印出第三步放的 'async1 end'
  7. 因为微任务列队会一路执行到没东西,所以继续看微任务列队,发现里面还有刚刚第四步骤放入的 resolve 代码,所以印出 'promise2'
  8. 这时微任务列队空了,去看宏任务列队,有第一步放入宏任务列队的 setTimeout 所以把它印出