准备面试的时候有遇到这个问题,读了好多文章(包括大佬的、总结大佬们的)都还是没搞懂(资质愚笨),最后还是这篇文章让我有更多清晰的认识,在这也总结一下。
一、什么是事件循环
因为Javascript是单线程语言(一次只能执行一个任务),所以它必须有一套处理事件/任务执行顺序的规则,避免单一任务执行过久,阻塞主程序的执行。JS在执行时,会有包含可以立即执行(同步)的任务(task),也有需要耗时(异步)的任务(setTimeout、XHR),所以JS有一种机制叫事件循环(Event loop)来检查这些任务放在了什么地方(后面细讲),来依次按规则的执行。
二、不同任务放在不同地方
像是那些不需耗时、可以同步执行的任务,都会放在执行栈中(call stack)中直接执行。而那些Web APIs(setTimeout、XHR)这些由浏览器提供的API,浏览器在先执行它们。执行(比如:setTimeout)后(比如setTimeout倒数的时间到了、请求的资源返回了)再把这些API内的回调函数(callback)加入到任务队列(task queue)中等待处理。
三、事件循环解决了什么问题?微任务和宏任务?
前面说到,这些回调函数已经进入了任务队列,那么等到什么时候,以什么顺序才能入栈呢?这就是事件循环要解决的问题。
自从ES6之后,有了新的处理异步任务的方法——Promise。有了这个Promise之后,事件循环多了一个新的角色来处理Promise里的then和catch这类的任务,即微任务(microtask),叫做微任务队列(microtask queue)。而原本的任务队列就变成了宏任务队列(macrotask queue),所以原来的Web APIs都是宏任务(macrotask)了。
注:Vue中的
MutationObserver和Node.js中的process.nextTick都属于microtask
四、每一次事件循环的步骤顺序
- 每一次的循环,会先从宏任务队列拿出一个最早放进去的宏任务(oldest task)执行(注:引入的
script也算宏任务) - 当这个宏任务执行完后,再来检查微任务队列是否有任务。有的话先执行微任务,如果执行完后产生新的微任务,新的微任务会接着上一个微任务后继续执行,直到当前循环内所有的微任务执行完成。
- 微任务执行完后,开始画面的渲染。
- 如果没有宏任务的话,就结束目前所有的任务。如果还有,就返回第一个步骤。
五、实例说明
setTimeout(() => console.log("timeout 1"));
Promise.resolve()
.then(() => console.log("promise 1"));
Promise.resolve()
.then(() => console.log("promise 2"));
setTimeout(() => console.log("timeout 2"));
console.log("code");
// code -> promise 1 -> promise 2 -> timeout 1 -> timeout 2
执行细节:
- 引入目前JS脚本,放进宏任务队列
- 将刚刚的宏任务取出,开始执行内部的代码(打印
code) - 接着把微任务队列内的任务取出并执行(打印
promise 1,promise 2) - 执行微任务后,发现宏任务队列还有任务(
setTimeout1,setTimeout2倒数结束,回调函数加入宏任务队列) - 开始新的事件循环,将最早放进去宏任务执行(打印
timeout 1) - 检查是否有微任务,如果没有结束目前的循环
- 发现还有一个宏任务
- 再开始新的事件循环,将下一个宏任务执行(打印
timeout 2) 看起来时promise(微任务)执行后才执行timeout(宏任务)。但是第一个打印的code,其实是从第一个宏任务——引入目前的script
六、注意
微任务执行完后才会开始渲染画面,所以放在微任务里的内容要注意。里面的内容可能会影响性能,导致画面卡住,无法渲染也无法操作,影响用户体验。
七、小测试
一道来自字节前端实习面试题,读完这篇文章以后你知道答案吗
console.log('start');
setTimeout(() => {
console.log('timer1');
Promise.resolve().then(function () {
console.log('promise1');
});
}, 0);
setTimeout(() => {
console.log('timer2');
Promise.resolve().then(function () {
console.log('promise2');
});
}, 0);
Promise.resolve().then(function () {
console.log('promise3');
});
console.log('end');