事情发生在今年上半年,我参加了科大讯飞合肥总部的前端面试,属于线上面,会议上对方给出了几道题,涉及到“事件循环Event Loop”看图求解,细化一点就是【微任务、宏任务流程及结果推算】。
当时我答错了一个,现场对方也给出了指正,一面结束,针对JS事件循环机制,疯狂回顾+恶补,又刷了很多练习题,感觉自己这一块重新捡起来了。
结果令人悲伤的是,几个月之后,我发现我又忘了......(呜呜呜)
相信部分人也有同样的错觉,莫不是我只有七秒记忆?
事件循环在前端一面部分,属于高频考点,特别是面试官,非常喜欢拉出来两道题,让你秀操作。
所以为了防止再次遗忘,彻底把这块内容,完全吃透+打通,做一期【事件循环最强攻略】!
攻略指示牌:前半部分,彻底剖析事件循环的底层逻辑,后半部分,囊括全网各种偏、难、怪习题,来一次统一操练,经过这种打磨,相信你以后再也忘不掉,一面直接丝滑连招开秀!
一.对于事件循环、微任务、宏任务等问题,网上有很多,但往往读起来晦涩,理论性很强。因此这一块我以实践的角度,用最简洁、易理解的方式,将整套流程梳理出来,宝子你可以放心看!
首先JS是一门单线程语言,这就意味着,所有任务会按照先后顺序来执行,上一个没执行完,那么下一个就得等着。
可矛盾点在于,如果js执行时间过长,那么页面渲染就会出问题(如加载阻塞),举个例子。
为了解决这种问题,同步任务、异步任务的理念由此诞生。
同步任务很好理解,它会在主线程,按照从上到下、从先到后的顺序执行,如此反复执行,其中同步任务会在执行栈中执行。(这里有个名词,叫主线程和执行栈)
异步任务有点讲究(他虽然也得在主线程运行,毕竟js是单线程),但为了让任务能够同时执行,异步任务会被放在【任务队列】中。(注意,这里出现了另一个名词——任务队列(有的人叫事件队列))
如果任务队列中的异步任务执行完了,那么会通过“回调函数(callback)”的形式,通知主线程.
也就是说,执行栈中的同步任务执行完了以后,会去轮询任务队列中的异步任务,将之放在执行栈中去执行,这个过程不断循环进行,因此,也叫事件轮询或事件循环。
具体我做了一张图,如图所示:
对于上述回调函数概念的理解:
如果主线程遇到异步任务,会把异步任务的回调函数放在任务队列,然后等主线程的同步任务执行完,再去任务队列看执行完的异步任务,按照先进先出,后进后出的顺序,依次执行回调函数,加之在主线程并运行。
这些回调函数最初的产生原因,主要是当你点击、页面刷新、操作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
在每一次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