【高频】科大讯飞一面,是否了解事件循环?谈谈微任务、宏任务概念!

234 阅读8分钟

事情发生在今年上半年,我参加了科大讯飞合肥总部的前端面试,属于线上面,会议上对方给出了几道题,涉及到“事件循环Event Loop看图求解,细化一点就是【微任务、宏任务流程及结果推算】。

当时我答错了一个,现场对方也给出了指正,一面结束,针对JS事件循环机制,疯狂回顾+恶补,又刷了很多练习题,感觉自己这一块重新捡起来了。

结果令人悲伤的是,几个月之后,我发现我又忘了......(呜呜呜)

相信部分人也有同样的错觉,莫不是我只有七秒记忆?

事件循环在前端一面部分,属于高频考点,特别是面试官,非常喜欢拉出来两道题,让你秀操作。

所以为了防止再次遗忘,彻底把这块内容,完全吃透+打通,做一期【事件循环最强攻略】!

攻略指示牌:前半部分,彻底剖析事件循环的底层逻辑,后半部分,囊括全网各种偏、难、怪习题,来一次统一操练,经过这种打磨,相信你以后再也忘不掉,一面直接丝滑连招开秀

微信图片_20241107145258.gif

一.对于事件循环、微任务、宏任务等问题,网上有很多,但往往读起来晦涩,理论性很强。因此这一块我以实践的角度,用最简洁、易理解的方式,将整套流程梳理出来,宝子你可以放心看!

首先JS是一门单线程语言,这就意味着,所有任务会按照先后顺序来执行,上一个没执行完,那么下一个就得等着

可矛盾点在于,如果js执行时间过长,那么页面渲染就会出问题(如加载阻塞),举个例子。

image.png

为了解决这种问题,同步任务、异步任务的理念由此诞生。

同步任务很好理解,它会在主线程,按照从上到下、从先到后的顺序执行,如此反复执行,其中同步任务会在执行栈中执行。(这里有个名词,叫主线程和执行栈

异步任务有点讲究(他虽然也得在主线程运行,毕竟js是单线程),但为了让任务能够同时执行,异步任务会被放在【任务队列】中。(注意,这里出现了另一个名词——任务队列(有的人叫事件队列)

如果任务队列中的异步任务执行完了,那么会通过“回调函数(callback)”的形式,通知主线程.

也就是说,执行栈中的同步任务执行完了以后,会去轮询任务队列中的异步任务,将之放在执行栈中去执行,这个过程不断循环进行,因此,也叫事件轮询或事件循环

具体我做了一张图,如图所示:

image.png

对于上述回调函数概念的理解:

如果主线程遇到异步任务,会把异步任务的回调函数放在任务队列,然后等主线程的同步任务执行完,再去任务队列看执行完的异步任务,按照先进先出,后进后出的顺序,依次执行回调函数,加之在主线程并运行。

这些回调函数最初的产生原因,主要是当你点击、页面刷新、操作DOM、鼠标事件等等操作后,才出现的。

简单总结一下:

到了这一步,就很好理解了,原先的流程是同步异步掺杂在一起,由于异步运行时间长,会阻塞页面。

而现在是把异步任务摘出去,主线程只留下同步任务,同步异步在不同的地方,同时运行,等同步任务完事之后,再把运行好的异步任务拿过来,合并完成渲染,这样页面加载速度就快了。(很好理解)

需要注意的是,咱们研究的是JS事件循环,所以同步任务和异步任务,都是JS的部分,也就是script包括的内容。

再深入思考一下,异步任务所谓的任务队列,它是不是线程上执行的?(答案:是的)

虽然JS是单线程,但浏览器它是多线程的,因此当你需要调接口的时候,浏览器会启动一个Http请求线程,去执行这个异步任务,而JS主线程还是不变,依然执行那些同步任务,这里的概念要区分开。

浏览器除了Http请求线程,还有定时器线程等,用以执行不同类型的异步任务。

同步任务现在咱们不看了,针对异步任务这个点,再剖析一下

异步任务这一块分为**【宏任务】+【微任务】**

至于执行的先后顺序,其实看你怎么说,一般是先执行微任务,然后再执行下一个宏任务。

反之,也有一种说法是先执行当前的宏任务,再执行里面的微任务,因为script代码也属于宏任务,当真提升大差不差,都一样,重点是要解释清楚。(另外script你也可以理解为同步外层代码)

宏任务(macrotask)主要指代:

script(整体代码)、setTimeout、setInterval、UI 渲染、 I/O、postMessage、 MessageChannel、setImmediate(Node.js 环境)、Ajax、DOM事件

微任务(microtask)主要指代:

Promise、 MutaionObserver、process.nextTick(Node.js环境)、async/await

image.png

在每一次JS事件循环(Event Loop)的执行过程中:

1.第一步会进入到script标签,遇到同步任务会立即执行。

2.遇到宏任务,会放入到宏任务队列中。

3.遇到微任务,会放到微任务队列中

4.再往下,如果有同步任务,接着执行,直至执行完毕

5.回过头执行为任务代码

6.微任务代码执行完毕后,说明当前宏任务执行完毕,本次队列清空

7.再次去找下一个宏任务,如此反复

整个不断执行的过程,就称为事件循环(照应开篇的叙述)

为了更直观感受两者的实际效果,做点面试题试一试

1.题目1(先来个简单的试试水)

console.log(100)
 // 宏任务
 setTimeout(()=>{
     console.log(200)
 })
 // 微任务
 Promise.resolve().then(()=>{
     console.log(300)
 })
 console.log(400)

答案:

100
400
300
200

首先第一个代码console.log,属于JS同步任务,正常在主线程执行,直接输出100。
再往下碰到定时器,确定为宏任务,加入到宏任务队列,暂时不管。
其次往下走,看到.then微任务,加入到微任务队列,暂时不管。
最后console.log属于同步任务,正常运行,输出400。
同步任务执行完,回头去找该宏任务下的微任务,要执行完。
所以输出300。
微任务全部执行完之后,找下一个宏任务,那么就是一开始的定时器。(第一个宏任务为sript)
因此最后输出200

2.题目2(有点上难度了)

promise本身是同步执行的,而then是异步执行

 (() => {
      setTimeout(() => {
        console.log(0);
      });

      new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve();
          Promise.resolve().then(() => {
            console.log(2);
          });
          console.log(3);
        });
      }).then(() => {
        console.log(5);
        Promise.resolve().then(() => {
          console.log(8);
        });
        setTimeout(() => {
          console.log(6);
        });
      });
    })();

答案:

0
3
5
2
8
6

首先从上往下,碰到定时器,放在宏任务队列
new Promise里面有定时器,放在宏任务队列
再往下没有了,.then没回调,暂不执行,因此往回看宏任务队列
所以输出最开始的定时器,结果为0
再找第二个宏任务,此时走到resolve()
它会改变promise状态,并把.then推入微任务队列
下面daiconsole.log(2)的promise,放到微任务队列
再往下执行consle.log(3),输出3
此时同步任务解决,去找微任务,也就是.then的内容
第一个console.log(5)成功输出
带console.log(8)的promise放到微任务队列
之前带console.log的promise按顺序取出,因此输出2
最后微任务还剩一个,就是console.log(8)的微任务,因此输出8
宏任务最后还剩一个,那就是最后的定时器,输出6

3.题目3(这个细心一点,做对的话,其余类似题型都是小意思了)

setTimeout(() => {
  console.log(1)
}, 0)
new Promise(resolve => {
  console.log(2)
  resolve()
  console.log(3)
}).then(() => {
  console.log(4)
})
const promise2 = new Promise(async resolve => {
  console.log(await resolve(5))
  console.log(6)
})
async function test () {
  console.log(7)
  console.log(await promise2)
  console.log(8)
}
test()
console.log(9)

答案:

2
3
7
9
4
undefined
6
5
8
1

4.题目4

setTimeout(function () {
  console.log("set1");
  new Promise(function (resolve) {
    resolve();
  }).then(function () {
    new Promise(function (resolve) {
      resolve();
    }).then(function () {
      console.log("then4");
    });
    console.log("then2");
  });
});

new Promise(function (resolve) {
  console.log("pr1");
  resolve();
}).then(function () {
  console.log("then1");
});

setTimeout(function () {
  console.log("set2");
});

console.log(2);

queueMicrotask(() => {
  console.log("queueMicrotask1")
});

new Promise(function (resolve) {
  resolve();
}).then(function () {
  console.log("then3");
});

答案:

pr1
2
then1
queuemicrotask1
then3
set1
then2
then4
set2

5.题目5

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
aysnc1 end
promise2
setToueout