背景
JS的事件循环是一个看似简单却又容易被误解的逻辑。这个概念对于我们日常开发中的性能优化、异步编程等方面有着至关重要的影响。
事件循环是个啥?
JavaScript 是单线程语言,这意味着它一次只能执行一个任务。但是,我们经常会碰到需要处理异步操作的情况,比如请求数据、设置定时器等等。这时候,JS的事件循环就派上用场了,它帮助我们管理这些异步操作。
事件循环的核心在于,“宏任务”(Macro-task)和“微任务”(Micro-task)的概念。简单来说:
宏任务包括:
setTimeoutsetIntervalsetImmediate(仅在 Node.js 中)I/O操作(如文件读写、网络请求等,通常在 Node.js 中)- 用户交互事件(如点击、滚动等)
requestAnimationFrame(主要用于浏览器的动画效果)MessageChannelpostMessage(用于 Web Workers)
微任务包括:
Promise.then,Promise.catch,Promise.finallyprocess.nextTick(仅在 Node.js 中)MutationObserver(用于监听 DOM 变更)queueMicrotask(直接将任务加入微任务队列的标准方法)
宏任务和微任务的执行顺序
JS的事件循环遵循一个简单的规则,这个规则决定了宏任务和微任务何时何地执行。
执行规则:
- 从宏任务队列中取出一个宏任务执行。
- 执行完这个宏任务后,执行环境会检查微任务队列。
- 如果微任务队列中有任务,执行环境会一直清空微任务队列,即依次执行微任务,直到微任务队列为空。
- 微任务队列清空后,浏览器可能会进行渲染操作(如果有必要)。
- 然后再从宏任务队列中取出下一个宏任务,重复上述过程。
划重点:
- 宏任务在每次只执行一个,每个宏任务之间可以插入渲染。
- 微任务则是在每个宏任务后连续执行,直到队列清空,微任务生成的新微任务也会在当前轮次执行。
好了,废话不多说,我们直接上代码看看实际怎么回事。
一些DEMO
一个简单的案例
console.log('开始');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('Promise');
});
console.log('结束');
给你5s想想这段代码最终的输出是什么?
应该难不倒你:
开始
结束
Promise
setTimeout
解析:
console.log('开始')是同步代码,立即执行。setTimeout是宏任务,被放到事件队列等待下一个循环。Promise.resolve()则会生成一个微任务,这个微任务会在当前宏任务结束后、下一个宏任务开始前执行。console.log('结束')同样是同步代码,紧接着执行。- 当当前宏任务执行完毕,事件循环会先处理所有的微任务(这里是输出
第一个 Promise)。 - 微任务执行完后,再执行下一个宏任务(输出
第一个 setTimeout)。
一个稍微复杂的demo
console.log('1');
setTimeout(function() {
console.log('2');
Promise.resolve().then(function() {
console.log('3');
});
console.log('4');
});
new Promise(function(resolve) {
console.log('5');
resolve();
}).then(function() {
console.log('6');
});
console.log('7');
解析
- 首先,同步代码
1,5,7依次执行。 - 完成同步代码后,处理所有生成的微任务(
6)。 - 接下来,处理第一个宏任务中的同步部分
2和4。 - 然后处理该宏任务中产生的微任务
3。