Javascript
事件循环
浏览器中的事件循环
JavaScript 是单线程语言,只能做一件事。但是浏览器还要做很多任务(比如监听点击、执行定时器等),所以引入了 事件循环机制 来协调这些任务。
JavaScript 执行过程简述
同步任务:立即执行,放在主线程中(主调用栈)。
异步任务:交给浏览器处理(如定时器、DOM 事件、网络请求等),完成后放入任务队列,等待执行。
两类任务队列
| 类型 | 描述 | 示例 |
|---|---|---|
| 宏任务(Macro Task) | 主体任务,按顺序执行 | setTimeout、setInterval、事件绑定 |
| 微任务(Micro Task) | 宏任务之后立即执行的小任务,优先级更高 | Promise.then、MutationObserver |
事件循环机制的执行步骤
- 执行主线程的同步任务(调用栈)。
- 清空所有微任务队列(Micro Task Queue)。
- 执行一个宏任务(Macro Task Queue)中的下一个任务。
- 重复步骤 2 和 3。
[主线程 Stack]
↓
执行同步任务
[微任务队列]
→ 全部清空(比如 Promise)
[宏任务队列]
→ 执行一个(比如 setTimeout)
然后继续循环...示例
JavaScript是单线程的,但通过事件循环机制实现了异步操作。浏览器中的事件循环主要由以下部分组成:
// 调用栈(Call Stack):同步任务在此执行
function main() {
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
console.log('3');
}
main(); // 输出顺序:1, 3, 2浏览器事件循环包含以下队列:
- 微任务队列(Microtask Queue):Promise回调、MutationObserver等
- 宏任务队列(Macrotask Queue):setTimeout、setInterval、DOM事件等
console.log('同步任务1');
setTimeout(() => {
console.log('宏任务1');
}, 0);
Promise.resolve().then(() => {
console.log('微任务1');
setTimeout(() => {
console.log('宏任务2');
}, 0);
Promise.resolve().then(() => {
console.log('微任务2');
});
});
console.log('同步任务2');
// 输出顺序:
// 同步任务1
// 同步任务2
// 微任务1
// 微任务2
// 宏任务1
// 宏任务2Node.js中的事件循环
Node.js事件循环基于libuv,比浏览器更为复杂,包含多个阶段:
// Node.js事件循环的六个阶段
// 1. timers: setTimeout和setInterval回调
// 2. pending callbacks: 执行系统操作的回调,如TCP错误
// 3. idle, prepare: 内部使用
// 4. poll: 获取新的I/O事件,执行I/O相关回调
// 5. check: setImmediate回调在这执行
// 6. close callbacks: 关闭事件的回调,如socket.on('close',...)
setImmediate(() => {
console.log('setImmediate');
});
setTimeout(() => {
console.log('setTimeout');
}, 0);
// 在主模块中顺序不确定
// 在I/O回调中,setImmediate总是先执行
fs.readFile('文件.txt', () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
// 在I/O回调中输出顺序:immediate, timeout浏览器与Node.js事件循环的区别
// 浏览器中:每个宏任务执行完后,会清空所有微任务
// Node.js中:每个阶段完成后才会检查微任务队列
// 浏览器中的Promise微任务优先级高于所有宏任务
Promise.resolve().then(() => console.log('Promise'));
setTimeout(() => console.log('setTimeout'), 0);
// 浏览器输出:Promise, setTimeout
// Node.js中的特殊API:process.nextTick()
// nextTick优先级高于所有微任务
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('Promise'));
// Node.js输出:nextTick, Promise浏览器和Node.js事件循环的优缺点
浏览器优点:
// 简单直观,始终先执行所有微任务,再执行下一个宏任务
// 适合UI交互场景,确保用户操作得到及时响应
document.getElementById('button').addEventListener('click', () => {
Promise.resolve().then(() => {
// UI更新微任务将在任何setTimeout之前执行
console.log('处理用户点击');
});
});Node.js优点:
// 分阶段执行,更精细的控制
// 适合I/O密集型应用,可有效管理资源
// process.nextTick提供优先级最高的异步操作
function apiCall(arg, callback) {
if (typeof arg !== 'string')
return process.nextTick(callback, new TypeError('参数必须是字符串'));
// 正常处理
fs.readFile(arg, callback);
}浏览览事件循环顺序/优先级是
// 浏览器事件循环执行顺序与优先级
// 1. 首先执行所有同步代码(主线程/调用栈中的代码)
console.log('1. 同步代码最先执行');
// 2. 调用栈清空后,检查微任务队列(microtask queue)并执行所有微任务
// 微任务包括:Promise回调、MutationObserver、queueMicrotask()等
Promise.resolve().then(() => {
console.log('2. 微任务优先级高于宏任务');
});
// 3. 微任务队列清空后,从宏任务队列(macrotask queue)中取出一个宏任务执行
// 宏任务包括:setTimeout、setInterval、requestAnimationFrame、UI渲染等
setTimeout(() => {
console.log('3. 宏任务在微任务之后执行');
// 4. 该宏任务执行完毕后,再次检查并执行所有微任务
Promise.resolve().then(() => {
console.log('4. 宏任务之后会检查新产生的微任务');
});
}, 0);
// 5. 一次事件循环:同步代码 -> 微任务队列 -> 一个宏任务 -> 微任务队列 -> ...
// 完整执行顺序示例
console.log('同步1');
setTimeout(() => {
console.log('宏任务1');
Promise.resolve().then(() => {
console.log('宏任务1产生的微任务');
});
}, 0);
Promise.resolve().then(() => {
console.log('微任务1');
setTimeout(() => {
console.log('微任务产生的宏任务');
}, 0);
});
console.log('同步2');
// 输出顺序:
// 同步1
// 同步2
// 微任务1
// 宏任务1
// 宏任务1产生的微任务
// 微任务产生的宏任务
// 浏览器中微任务优先级:
queueMicrotask(() => console.log('queueMicrotask微任务'));
Promise.resolve().then(() => console.log('Promise微任务'));
// 二者属于同一优先级,按代码顺序执行
// 宏任务优先级(从高到低):
// 1. 用户交互事件(如点击)
// 2. requestAnimationFrame
// 3. setTimeout/setInterval定时器的调度机制
在每一轮事件循环中,浏览器会检查:当前时间是否已经达到或超过 setTimeout 所设定的执行时间点(当前注册时间 + delay),如果是,就把它加入宏任务队列,等待执行。
// 模拟ajax
const fakeAjax = (t=1000)=> {
return new Promise((resolve, reject) => {
setTimeout(()=>{
console.log('sleep',t)
resolve()
},t)
})
}
setTimeout(()=>{
console.log('setTimeout 0')
},0)
fakeAjax()
setTimeout(()=>{
console.log('setTimeout 100')
},100)