在前端开发中,JavaScript 的事件循环(Event Loop)机制一直是面试和实际开发中的高频考点。很多初学者对进程、线程、同步、异步、微任务、宏任务等概念容易混淆。本文将结合具体例子,深入浅出地讲清楚这些核心知识点,帮助你彻底掌握事件循环的本质。
进程与线程:浏览器的幕后英雄
进程是操作系统分配资源的最小单位。比如你在手机上打开微信,这个动作就启动了一个进程,从打开到关闭,微信都在这个进程中运行。
线程是进程中的更小单位。一个进程可以包含多个线程,各司其职。例如微信的聊天界面需要渲染线程来显示内容,同时还需要网络线程来接收新消息。
在浏览器中,每打开一个新的 Tab 页面,实际上就是启动了一个新的进程。每个进程内部又有多个线程协同工作,比如:
- HTTP 线程:负责网络请求
- JS 引擎线程:负责执行 JavaScript 代码
- 渲染线程:负责页面渲染
需要注意的是,JS 引擎线程和渲染线程是互斥的,即同一时刻只能有一个在工作,避免页面渲染和 JS 执行互相干扰。
JavaScript 的单线程与异步
JavaScript 设计之初就是单线程,即同一时刻只能有一个任务在执行。这是因为 JS 主要用于与页面交互,如果多线程同时操作 DOM,容易引发混乱。
但现代 Web 应用需要处理大量异步任务,比如网络请求、定时器等。为了解决这个问题,JavaScript 引入了异步机制,即把耗时操作交给浏览器其他线程处理,等结果出来后再通知 JS 引擎线程处理。
事件循环(Event Loop):JS 的调度员
事件循环是 JS 实现异步的核心机制。它的执行流程如下:
- 执行同步代码,遇到异步任务(如 setTimeout、Promise)时,将其放入任务队列。
- 同步代码执行完毕后,先执行微任务队列(如 Promise.then、MutationObserver、process.nextTick)。
- 微任务全部执行完毕后,渲染页面(如有必要)。
- 渲染后,执行宏任务队列(如 setTimeout、setInterval、ajax、DOM 事件),然后开启下一轮事件循环。
任务队列的分类
- 微任务(Microtask):Promise.then、process.nextTick、MutationObserver、async/await ...
- 宏任务(Macrotask):setTimeout、setInterval、ajax、DOM 事件 等...
具体例子解析
让我们通过几个具体的 JS 代码例子,来直观理解事件循环的执行顺序。
例子一:同步与异步的基本执行顺序
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
console.log(3);
输出结果:
解析:
console.log(1)立即执行,输出 1。setTimeout是宏任务,回调被放入宏任务队列,等待主线程空闲时执行。console.log(3)继续执行,输出 3。- 同步代码执行完毕后,事件循环开始,执行宏任务队列中的回调,输出 2。
例子二:微任务与宏任务的优先级
console.log('start');
setTimeout(() => {
console.log('timeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise');
});
console.log('end');
输出结果:
解析:
start和end是同步代码,先输出。setTimeout回调进入宏任务队列。Promise.then回调进入微任务队列。- 同步代码执行完毕后,先清空微任务队列,输出
promise。 - 最后执行宏任务队列,输出
timeout。
例子三:async/await 的本质
async function test() {
console.log('a');
await Promise.resolve();
console.log('b');
}
test();
console.log('c');
输出结果:
解析:
test()执行,输出a。await后面的代码(console.log('b'))会被放入微任务队列。console.log('c')是同步代码,立即执行。- 同步代码执行完毕后,执行微任务队列,输出
b。
补充说明:
await 实际上会将后续代码“挤入”微任务队列,浏览器对 await 的处理甚至比普通 Promise.then 更加优先。
例子四:微任务与宏任务的嵌套
console.log('A');
setTimeout(() => {
console.log('B');
Promise.resolve().then(() => {
console.log('C');
});
}, 0);
Promise.resolve().then(() => {
console.log('D');
});
console.log('E');
输出结果:
解析:
A、E是同步代码,先输出。Promise.then的D进入微任务队列。setTimeout的回调进入宏任务队列。- 同步代码执行完毕,先执行微任务队列,输出
D。 - 然后执行宏任务队列,输出
B,并在宏任务回调中又插入一个微任务(C)。 - 当前宏任务执行完毕后,立即执行新插入的微任务,输出
C。
五、总结与复习要点
- 进程是资源分配的最小单位,线程是执行的最小单位。
- 浏览器每个 Tab 是一个进程,内部有多个线程协作。
- JavaScript 是默认单线程,异步任务通过事件循环机制实现。
- 事件循环的执行顺序:同步代码 → 微任务队列 → 渲染 → 宏任务队列。
- 微任务优先于宏任务,Promise.then、async/await 都属于微任务。
JavaScript 的事件循环机制是理解异步编程的核心。掌握进程、线程、同步、异步、微任务、宏任务等概念,并通过具体例子来加深理解,希望本文能成为你复习和查漏补缺的好帮手!