Hi,大家好,这里是JustHappy,JS异步相关问题是前端开发面试中有关JS基础必考的一个知识模块,我们需要在未来面试中完美的回答这方面的问题(当然这是追求啦)于是乎我写了这篇文章,带你也带我理清回答这方面问题的思路
我们就以其中最关键点一个“event loop”开始吧
假设面试官说:讲一下事件循环event loop吧...
那么你......
首先!你得搞明白JavaScript是个啥?
JavaScript是一个单线程语言!程序的执行流是线性的,一次只能执行一个任务。这意味着在一个给定的时间内,只有一个操作被执行,而其他操作必须等待当前操作完成。这也就带来了以下两个问题,或者说特性吧~
- 顺序执行:程序中的指令按照它们在代码中出现的顺序一个接一个地执行。
- 阻塞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机制下,我们的浏览器会先将同步代码放到一个(Call stack)事件栈中,将异步任务放到(Callback Queue)中,这是怎么个过程呢,咱和浏览器一样,本质上都是从上往下一行行看代码,当遇到同步的代码就放到栈里一个一个执行,遇到异步代码就放到一个事件队列中,等到同步事件都执行完了,也就是栈为空的时候,我们就看看事件队列中有没有哪一个异步任务响应成功了,如果响应成功,就将其出队。
总结起来我觉得有以下几点值得细分开来讲
-
事件循环(Event Loop)的作用:
- 事件循环会不断地检查调用栈是否为空。如果调用栈为空,事件循环会检查消息队列。
- 如果消息队列中有任务,事件循环会将这些任务放入调用栈中执行。
-
异步任务的触发:
- 异步任务的触发通常是由浏览器的事件触发机制决定的,比如定时器(setTimeout/setInterval)、事件监听器(如click事件)、网络请求(如Ajax请求)等。
-
宏任务和微任务:
- 需要注意的是,消息队列中的任务分为宏任务(Macro Tasks)和微任务(Micro Tasks)。宏任务包括setTimeout、setInterval、I/O、UI渲染等,而微任务包括Promise.then、MutationObserver等。
- 事件循环在一次循环中会先执行完所有的微任务,然后再执行宏任务。
-
栈为空时的行为:
- 当调用栈为空时,事件循环会检查消息队列,如果有宏任务就取出执行,而不是检查是否有异步任务响应成功。
总的过程我也找到了一个比较直观的图片,希望可以帮助到大家,图片的出处放在这里了
我们来练练手吧,以下是一个考查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);
我们来一行行看这个代码吧
-
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) => { ... }):这是同步代码是同步代码,立即执行,输出4。resolve()会触发Promise的.then()回调,这个回调是一个微任务,被放入微任务队列。then(() => { ... }):这是异步代码的入口点。console.log(5);:这是.then()方法的回调函数,它将在Promise对象状态变为resolved之后被加入到微任务队列中,等待当前执行栈清空后执行。
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