干货分享:理解JavaScript 的事件循环

88 阅读3分钟

背景

JS的事件循环是一个看似简单却又容易被误解的逻辑。这个概念对于我们日常开发中的性能优化、异步编程等方面有着至关重要的影响。

事件循环是个啥?

JavaScript 是单线程语言,这意味着它一次只能执行一个任务。但是,我们经常会碰到需要处理异步操作的情况,比如请求数据、设置定时器等等。这时候,JS的事件循环就派上用场了,它帮助我们管理这些异步操作。

事件循环的核心在于,“宏任务”(Macro-task)和“微任务”(Micro-task)的概念。简单来说:

宏任务包括:

  • setTimeout
  • setInterval
  • setImmediate(仅在 Node.js 中)
  • I/O操作(如文件读写、网络请求等,通常在 Node.js 中)
  • 用户交互事件(如点击、滚动等)
  • requestAnimationFrame(主要用于浏览器的动画效果)
  • MessageChannel
  • postMessage(用于 Web Workers)

微任务包括:

  • Promise.then, Promise.catch, Promise.finally
  • process.nextTick(仅在 Node.js 中)
  • MutationObserver(用于监听 DOM 变更)
  • queueMicrotask(直接将任务加入微任务队列的标准方法)

宏任务和微任务的执行顺序

JS的事件循环遵循一个简单的规则,这个规则决定了宏任务和微任务何时何地执行。

执行规则:

  1. 从宏任务队列中取出一个宏任务执行。
  2. 执行完这个宏任务后,执行环境会检查微任务队列。
  3. 如果微任务队列中有任务,执行环境会一直清空微任务队列,即依次执行微任务,直到微任务队列为空。
  4. 微任务队列清空后,浏览器可能会进行渲染操作(如果有必要)。
  5. 然后再从宏任务队列中取出下一个宏任务,重复上述过程。

划重点:

  • 宏任务在每次只执行一个,每个宏任务之间可以插入渲染。
  • 微任务则是在每个宏任务后连续执行,直到队列清空,微任务生成的新微任务也会在当前轮次执行。

好了,废话不多说,我们直接上代码看看实际怎么回事。

一些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)。
  • 接下来,处理第一个宏任务中的同步部分 24
  • 然后处理该宏任务中产生的微任务 3