「滴滴面试官👩‍💻」讲一下事件循环event loop......结果我就😭😭😭😭(一篇文章带你、带我理清面试回答有关【JS异步问题】的思路)

184 阅读6分钟

Hi,大家好,这里是JustHappy,JS异步相关问题是前端开发面试中有关JS基础必考的一个知识模块,我们需要在未来面试中完美的回答这方面的问题(当然这是追求啦)于是乎我写了这篇文章,带你也带我理清回答这方面问题的思路

我们就以其中最关键点一个“event loop”开始吧

假设面试官说:讲一下事件循环event loop吧...

那么你......

首先!你得搞明白JavaScript是个啥?

JavaScript是一个单线程语言!程序的执行流是线性的,一次只能执行一个任务。这意味着在一个给定的时间内,只有一个操作被执行,而其他操作必须等待当前操作完成。这也就带来了以下两个问题,或者说特性吧~

  1. 顺序执行:程序中的指令按照它们在代码中出现的顺序一个接一个地执行。
  2. 阻塞I/O:在传统的单线程编程模型中,如果遇到I/O操作(如读取文件、网络请求等),程序会等待I/O操作完成才能继续执行后续代码,这可能导致程序在等待I/O时无响应。

我们的需求是什么?

经常写业务的同学应该都处理过“异步”,比方说发起一个post请求,等待后端响应,但是我们在等待的过程中不可能啥都不干吧!如果啥都不干,那就是我们上面提到的阻塞I/O,我们这时候就需要“异步”操作,可在JavaScript是一个单线程语言的前提下,我们该如何进行“异步操作”实现“非阻塞I/O”呢?

于是乎我们有了事件循环......

啥是事件循环event loop?

这其实是浏览器运行JavaScript的一个机制,其目的就是为了使得JavaScript可以处理异步和实现 “非阻塞I/O” 的,这里值得注意的一点是,JavaScript是单线程的,咱的浏览器和Node.js可不是单线程的

事件循环干了什么?

我找了一张图片,可以比较详细的展示event loop的过程,下面我们来一个一个的拆解这个图吧

这是图片的出处 www.webdevolution.com/blog/Javasc…

Event-Loop-browser-V8.png

我们先执行同步代码!!

在event loop机制下,我们的浏览器会先将同步代码放到一个(Call stack)事件栈中,将异步任务放到(Callback Queue)中,这是怎么个过程呢,咱和浏览器一样,本质上都是从上往下一行行看代码,当遇到同步的代码就放到栈里一个一个执行,遇到异步代码就放到一个事件队列中,等到同步事件都执行完了,也就是栈为空的时候,我们就看看事件队列中有没有哪一个异步任务响应成功了,如果响应成功,就将其出队。

总结起来我觉得有以下几点值得细分开来讲

  1. 事件循环(Event Loop)的作用

    • 事件循环会不断地检查调用栈是否为空。如果调用栈为空,事件循环会检查消息队列。
    • 如果消息队列中有任务,事件循环会将这些任务放入调用栈中执行。
  2. 异步任务的触发

    • 异步任务的触发通常是由浏览器的事件触发机制决定的,比如定时器(setTimeout/setInterval)、事件监听器(如click事件)、网络请求(如Ajax请求)等。
  3. 宏任务和微任务

    • 需要注意的是,消息队列中的任务分为宏任务(Macro Tasks)和微任务(Micro Tasks)。宏任务包括setTimeout、setInterval、I/O、UI渲染等,而微任务包括Promise.then、MutationObserver等。
    • 事件循环在一次循环中会先执行完所有的微任务,然后再执行宏任务
  4. 栈为空时的行为

    • 当调用栈为空时,事件循环会检查消息队列,如果有宏任务就取出执行,而不是检查是否有异步任务响应成功。

总的过程我也找到了一个比较直观的图片,希望可以帮助到大家,图片的出处放在这里了

image.png

我们来练练手吧,以下是一个考查JavaScript异步问题的面试题

这是我前端生涯的第一场面试,面试的是滴滴,当然没过啦~~~,不过现在回想一下这道题还是很好的

请写出以下代码的打印顺序

console.log(1);

setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3);
  });
}, 0);

new Promise((resolve) => {
  console.log(4);
  resolve();
}).then(() => {
  console.log(5);
  setTimeout(() => console.log(6), 0);
});

console.log(7);

我们来一行行看这个代码吧

  1. console.log(1);:这是同步代码,首先执行,输出1

setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3);
  });
}, 0);

这是一个宏任务,它的回调函数被放入宏任务队列。由于setTimeout的延迟时间是0,它将在当前执行栈清空后的所有微任务执行完后尽快执行。

new Promise((resolve) => {
  console.log(4);
  resolve();
}).then(() => {
  console.log(5);
  setTimeout(() => console.log(6), 0);
});
  • new Promise((resolve) => { ... }):这是同步代码是同步代码,立即执行,输出4resolve()会触发Promise.then()回调,这个回调是一个微任务,被放入微任务队列。
  • then(() => { ... }):这是异步代码的入口点。
  • console.log(5);:这是.then()方法的回调函数,它将在Promise对象状态变为resolved之后被加入到微任务队列中,等待当前执行栈清空后执行。
  1. console.log(7);:这是同步代码,最后执行,输出7

好的目前我们看看输出了啥

1
4
7

我们现在先看看有什么微任务要执行

于是我们输出结果为

1
4
7
5

到这我们再看看

new Promise((resolve) => {
  console.log(4);
  resolve();
}).then(() => {
  console.log(5);
  setTimeout(() => console.log(6), 0);
});

我们发现.then()里还有一个setTimeout(),我们这时候再把他放到宏任务队列中,目前来看,微任务队列已经没有东西了,我们就开始执行宏任务队列

于是我们执行了以下setTimeout()中的内容

setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3);
  });
}, 0);

可以看到这里面直接输出了一个2,于是目前的输出结果是:

1
4
7
5
2

可以看到在这个代码中我们又遇到了Promise.then()回调,这个回调是一个微任务,我们把他放入微任务队列。

既然有了个微任务,微任务要先执行,于是我们输出了3,目前的结果是

1
4
7
5
2
3

ok,最后只剩下宏任务队列中有一个6了,那么输出结果就是

1
4
7
5
2
3
6