事件循环机制、同步异步、宏任务和微任务
一、基本概念
JavaScript 是单线程的
JavaScript 在设计之初就是单线程的,这意味着它一次只能执行一个任务。这种设计避免了多线程带来的复杂问题(如死锁、资源竞争等),但也意味着如果遇到耗时操作,整个程序可能会被阻塞。为什么需要事件循环?
为了解决单线程带来的阻塞问题,JavaScript 引入了事件循环机制。通过将任务分为同步任务和异步任务,并配合调用栈、任务队列等概念,JavaScript 可以在不阻塞主线程的情况下处理异步操作。
二、同步与异步
- 同步任务 (Synchronous)
同步任务是指在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
- 同步任务 (Synchronous)
console.log('1');
console.log('2');
console.log('3');
// 输出顺序永远是 1, 2, 3- 异步任务 (Asynchronous)
异步任务不进入主线程,而是进入任务队列。当主线程空闲时,事件循环会从任务队列中取出任务执行。
- 异步任务 (Asynchronous)
重要
常见的异步操作:
setTimeout/setInterval
AJAX 请求
Promise
DOM 事件
I/O 操作
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
// 输出顺序是 1, 3, 2三、事件循环机制
- 基本组成
调用栈 (Call Stack): 存储同步任务的执行上下文
任务队列 (Task Queue): 存储异步任务的回调
事件循环 (Event Loop): 监控调用栈和任务队列,当调用栈为空时,将任务队列中的任务推入调用栈
- 工作流程
同步任务在主线程顺序执行
遇到异步任务,交给对应的 Web API 处理(如定时器、HTTP 请求等)
Web API 处理完成后,将回调函数放入任务队列
当主线程(调用栈)为空时,事件循环从任务队列中取出任务执行
四、宏任务与微任务
宏任务 (MacroTask)
由宿主环境(浏览器/Node.js)发起的任务
常见的宏任务:
重要
setTimeout/setInterval
I/O 操作
UI 渲染
setImmediate (Node.js)
requestAnimationFrame (浏览器)
微任务 (MicroTask)
由 JavaScript 引擎发起的任务
常见的微任务:
重要
Promise.then/catch/finally
MutationObserver
process.nextTick (Node.js)
- 执行顺序规则
重要
执行一个宏任务(初始时是整个脚本)
执行过程中遇到微任务,加入微任务队列
宏任务执行完毕,立即执行所有微任务
微任务执行完毕,开始下一个宏任务
重复以上过程
- 示例分析
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
}, 0);
Promise.resolve().then(() => {
console.log('4');
setTimeout(() => {
console.log('5');
}, 0);
});
console.log('6');
// 输出顺序:1, 6, 4, 2, 3, **5**
1. 执行整个脚本(宏任务):
* 输出 1
* 遇到 setTimeout,将其回调加入宏任务队列
* 遇到 Promise.resolve().then(),将其回调加入微任务队列
* 输出 6
* 宏任务执行完毕
2. 执行微任务队列:
* 执行 Promise 回调,输出 4
* 遇到 setTimeout,将其回调加入宏任务队列
3. 微任务队列清空
* 执行下一个宏任务(第一个 setTimeout):
* 输出 2
* 遇到 Promise.resolve().then(),将其回调加入微任务队列
* 宏任务执行完毕
4. 执行微任务队列:
* 执行 Promise 回调,输出 3
* 微任务队列清空
5. 执行下一个宏任务(第二个 setTimeout):
* 输出 5五、更复杂的示例
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
// 输出顺序:
// script start
// script end
// promise1
// promise2
// setTimeoutconsole.log('同步代码1'); 1,执行第一个同步任务
setTimeout(() => {
console.log('setTimeout') 5,执行宏任务
}, 0)
new Promise((resolve) => {
console.log('同步代码2') 2,执行第二个同步任务
resolve()
}).then(() => {
console.log('promise.then') 4,执行微任务
})
console.log('同步代码3'); 3,执行第三个同步任务
// 最终输出"同步代码1"、"同步代码2"、"同步代码3"、"promise.then"、"setTimeout"setTimeout(() => {
console.log('setTimeout start'); // 5、执行宏任务里的第一个任务
new Promise((resolve) => {
console.log('promise1 start'); // 6、执行宏任务里的第二个任务
resolve();
}).then(() => {
console.log('promise1 end'); // 8、执行宏任务里的微任务
})
console.log('setTimeout end'); // 7、执行宏任务里的第三个任务
}, 0);
function promise2() {
return new Promise((resolve) => {
console.log('promise2'); // 2、执行第一个微任务
resolve();
})
}
async function async1() {
console.log('async1 start'); // 1、执行第一个同步任务
await promise2();
console.log('async1 end'); // 4、执行第二个微任务
}
async1();
console.log('script end'); // 3、执行第二个同步任务
**初始执行阶段(同步代码)**
1. async1 start:
* 首先调用 async1() 函数
* 执行 console.log('async1 start'),这是同步代码
2. promise2:
* 遇到 await promise2(),执行 promise2() 函数
* 在 promise2 中执行 console.log('promise2'),这是同步代码
* promise2 返回的 Promise 立即 resolve
3. script end:
* 继续执行 async1() 函数后的代码
* 执行 console.log('script end'),这是同步代码
**微任务阶段**
4. async1 end:
* 同步代码执行完毕后,开始处理微任务队列
* await promise2() 后面的代码被包装成一个微任务
* 执行 console.log('async1 end')
**宏任务阶段(setTimeout)**
5. setTimeout start:
* 微任务处理完毕后,开始执行宏任务队列
* setTimeout 回调函数开始执行
* 执行 console.log('setTimeout start')
6. promise1 start:
* 在 setTimeout 回调中创建新的 Promise
* 执行 console.log('promise1 start'),这是同步代码
* Promise 立即 resolve
7. setTimeout end:
* 继续执行 setTimeout 回调中的同步代码
* 执行 console.log('setTimeout end')
**微任务阶段(在宏任务内部)**
8. promise1 end:
* 当前宏任务执行完毕后,处理它产生的微任务
* Promise 的 then 回调是一个微任务
* 执行 console.log('promise1 end')重要
await 的行为本质上与 Promise 的 .then() 类似,它会将后面的代码包装成一个微任务。这是 JavaScript 异步编程模型的核心机制之一。让我们深入理解为什么这样设计:
- await 的底层实现
async function async1() {
console.log('async1 start');
await promise2();
console.log('async1 end');
}这实际上等价于:
function async1() {
console.log('async1 start');
return promise2().then(() => {
console.log('async1 end');
});
}- 对比没有 await 的情况
function sync1() {
console.log('sync1 start');
promise2(); // 没有await
console.log('sync1 end');
}
如果去掉 await,代码就是完全同步的:
执行顺序会是:
'sync1 start'
'promise2'
'sync1 end'六、Node.js 与浏览器的事件循环差异
- 浏览器环境
每次事件循环处理一个宏任务,然后处理所有微任务
UI 渲染在两个宏任务之间执行
- Node.js 环境
Node.js 的事件循环分为多个阶段:
timers 阶段:执行 setTimeout/setInterval 回调
pending callbacks:执行系统操作的回调
idle, prepare:内部使用
poll:检索新的 I/O 事件
check:执行 setImmediate 回调
close callbacks:执行关闭事件的回调
每个阶段执行完毕后,都会执行该阶段的微任务队列。
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// 输出顺序不确定,取决于事件循环的启动时间七、实际应用注意事项
避免长时间运行的同步任务:会阻塞事件循环
合理使用微任务:微任务会在当前宏任务结束后立即执行,可能导致性能问题
理解执行顺序:特别是在嵌套的 Promise 和 setTimeout 中
避免递归微任务:会导致无限循环
// 危险的递归微任务
function recursiveMicrotask() {
Promise.resolve().then(recursiveMicrotask);
}
// 这将阻塞主线程,导致页面无法响应八、总结
JavaScript 的事件循环机制是其异步编程的核心。理解同步/异步、宏任务/微任务的区别以及它们的执行顺序,对于编写高效、可预测的代码至关重要。记住以下关键点:
同步任务优先执行
异步任务分为宏任务和微任务
每个宏任务执行完毕后,会立即执行所有微任务
微任务优先级高于宏任务
不同环境(浏览器/Node.js)可能有细微差异