JavaScript 事件循环机制

JavaScript 事件循环机制详解

1. 事件循环基本概念

1.1 什么是事件循环?

事件循环是 JavaScript 处理异步任务的核心机制,它让单线程的 JavaScript 能够"同时"处理多个任务。

javascript 复制代码
console.log('1. 开始');

setTimeout(() => {
    console.log('2. 定时器');
}, 0);

Promise.resolve().then(() => {
    console.log('3. Promise');
});

console.log('4. 结束');

// 执行顺序:1 → 4 → 3 → 2
// 这就是事件循环在起作用!

1.2 为什么需要事件循环?

JavaScript 是单线程的,如果没有事件循环,遇到耗时任务时页面就会卡死。

javascript 复制代码
// 如果没有事件循环 - 糟糕的情况
console.log('开始');
const result = synchronousNetworkRequest(); // 假设这个请求需要3秒
console.log('收到结果:', result);
console.log('页面卡住了3秒!用户无法操作!');

// 有事件循环 - 良好的体验
console.log('开始');
asynchronousNetworkRequest((result) => {
    console.log('收到结果:', result);
});
console.log('页面可以立即响应!用户可以正常操作!');

2. 事件循环的组成部分

2.1 三大核心组件

javascript 复制代码
// 可视化事件循环结构
┌───────────────────────────┐
│       调用栈 (Call Stack)     │ ← 正在执行的代码
└───────────────────────────┘
             ↓
┌───────────────────────────┐
│      Web APIs 环境          │ ← 浏览器提供的异步API
│   - setTimeout          │
│   - DOM事件              │
│   - 网络请求              │
└───────────────────────────┘
             ↓
┌───────────────────────────┐
│       任务队列 (Task Queue)   │ ← 等待执行的回调函数
│   1. 宏任务队列              │
│   2. 微任务队列              │
└───────────────────────────┘
             ↓
┌───────────────────────────┐
│       事件循环 (Event Loop)   │ ← 协调调度的"管理员"
└───────────────────────────┘

2.2 调用栈 (Call Stack)

javascript 复制代码
function first() {
    console.log('第一个函数开始');
    second();
    console.log('第一个函数结束');
}

function second() {
    console.log('第二个函数开始');
    third();
    console.log('第二个函数结束');
}

function third() {
    console.log('第三个函数');
}

first();

// 调用栈变化:
// 1. first() 入栈
// 2. console.log() 入栈 → 执行 → 出栈
// 3. second() 入栈
// 4. console.log() 入栈 → 执行 → 出栈
// 5. third() 入栈
// 6. console.log() 入栈 → 执行 → 出栈
// 7. third() 出栈
// 8. console.log() 入栈 → 执行 → 出栈
// 9. second() 出栈
// 10. first() 出栈

3. 任务队列详解

3.1 微任务 (Microtasks) vs 宏任务 (Macrotasks)

javascript 复制代码
console.log('脚本开始'); // 同步任务

// 宏任务
setTimeout(() => {
    console.log('setTimeout - 宏任务');
}, 0);

// 微任务
Promise.resolve().then(() => {
    console.log('Promise - 微任务');
});

// 另一个微任务
queueMicrotask(() => {
    console.log('queueMicrotask - 微任务');
});

console.log('脚本结束'); // 同步任务

// 执行顺序:
// 1. 脚本开始 (同步)
// 2. 脚本结束 (同步)
// 3. Promise - 微任务
// 4. queueMicrotask - 微任务
// 5. setTimeout - 宏任务

3.2 微任务有哪些?

javascript 复制代码
// 常见的微任务来源:
Promise.then() / .catch() / .finally()
queueMicrotask()
MutationObserver(DOM变化观察)
process.nextTick(Node.js)

// 微任务特点:优先级高,在每个宏任务之后立即执行

3.3 宏任务有哪些?

javascript 复制代码
// 常见的宏任务来源:
setTimeout / setInterval
setImmediate(Node.js)
I/O 操作(文件读取、网络请求)
UI 渲染(浏览器)
DOM 事件(click、load等)

// 宏任务特点:优先级较低,等待调用栈清空后执行

4. 完整的事件循环流程

4.1 详细执行步骤

javascript 复制代码
// 步骤演示
console.log('1. 同步任务开始');

// 宏任务
setTimeout(() => {
    console.log('6. 宏任务 - setTimeout');
    Promise.resolve().then(() => {
        console.log('7. 微任务 - 在宏任务中');
    });
}, 0);

// 微任务
Promise.resolve().then(() => {
    console.log('4. 微任务 - Promise 1');
});

Promise.resolve().then(() => {
    console.log('5. 微任务 - Promise 2');
});

console.log('2. 同步任务继续');

setTimeout(() => {
    console.log('8. 另一个宏任务');
}, 0);

console.log('3. 同步任务结束');

// 执行过程分析:

4.2 事件循环算法

复制代码
1. 执行同步代码(调用栈)
2. 调用栈清空后,检查微任务队列
3. 执行所有微任务(直到微任务队列清空)
4. 必要时进行UI渲染
5. 从宏任务队列取一个任务执行
6. 回到步骤2,循环...

5. 实际代码演示

5.1 复杂示例分析

javascript 复制代码
console.log('start'); // 1. 同步

setTimeout(function() {
    console.log('timeout1'); // 5. 宏任务
    
    Promise.resolve().then(function() {
        console.log('promise1'); // 6. 微任务
    });
}, 0);

Promise.resolve().then(function() {
    console.log('promise2'); // 3. 微任务
    
    setTimeout(function() {
        console.log('timeout2'); // 7. 宏任务
    }, 0);
});

console.log('end'); // 2. 同步

// 输出顺序:
// start
// end
// promise2
// timeout1
// promise1
// timeout2

5.2 嵌套任务执行顺序

javascript 复制代码
console.log('1. 开始');

setTimeout(() => {
    console.log('2. 外层宏任务');
    
    Promise.resolve().then(() => {
        console.log('3. 外层微任务');
    });
    
    setTimeout(() => {
        console.log('4. 内层宏任务');
    }, 0);
}, 0);

Promise.resolve().then(() => {
    console.log('5. 外层微任务');
    
    setTimeout(() => {
        console.log('6. 微任务中的宏任务');
    }, 0);
});

console.log('7. 结束');

// 执行顺序分析:
// 1 → 7 → 5 → 2 → 3 → 6 → 4

6. 浏览器 vs Node.js 事件循环差异

6.1 浏览器事件循环

javascript 复制代码
// 浏览器中的阶段:
// 1. 执行同步代码
// 2. 执行微任务
// 3. UI渲染(如果需要)
// 4. 执行宏任务

console.log('脚本开始');

// 宏任务
setTimeout(() => console.log('计时器'));

// 微任务  
Promise.resolve().then(() => console.log('Promise'));

// 动画帧回调(在渲染前执行)
requestAnimationFrame(() => console.log('RAF'));

console.log('脚本结束');

6.2 Node.js 事件循环

javascript 复制代码
// Node.js 有更复杂的阶段:
// timers → pending callbacks → idle, prepare → poll → check → close callbacks

console.log('开始');

setTimeout(() => console.log('timer1'), 0);
setImmediate(() => console.log('immediate'));

process.nextTick(() => console.log('nextTick'));

Promise.resolve().then(() => console.log('promise'));

console.log('结束');

// Node.js 输出可能:
// 开始 → 结束 → nextTick → promise → timer1 → immediate
// 或者:开始 → 结束 → nextTick → promise → immediate → timer1

7. 常见面试题分析

7.1 经典面试题

javascript 复制代码
console.log('1');

setTimeout(() => {
    console.log('2');
    Promise.resolve().then(() => {
        console.log('3');
    });
}, 0);

new Promise((resolve) => {
    console.log('4');
    resolve();
}).then(() => {
    console.log('5');
});

console.log('6');

// 输出顺序:1 → 4 → 6 → 5 → 2 → 3

7.2 进阶面试题

javascript 复制代码
async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}

async function async2() {
    console.log('async2');
}

console.log('script start');

setTimeout(() => {
    console.log('setTimeout');
}, 0);

async1();

new Promise((resolve) => {
    console.log('promise1');
    resolve();
}).then(() => {
    console.log('promise2');
});

console.log('script end');

// 输出顺序:
// script start → async1 start → async2 → promise1 → script end
// → async1 end → promise2 → setTimeout

8. 性能优化实践

8.1 避免阻塞事件循环

javascript 复制代码
// 错误示范:同步耗时操作阻塞事件循环
function processLargeData(data) {
    // 这个函数执行时间很长,会阻塞页面
    for (let i = 0; i < 1000000; i++) {
        // 繁重的同步计算
    }
}

// 正确示范:将任务分解为异步执行
async function processLargeDataAsync(data) {
    for (let i = 0; i < data.length; i += 1000) {
        const chunk = data.slice(i, i + 1000);
        
        // 使用 setTimeout 或 Promise 让出控制权
        await new Promise(resolve => setTimeout(resolve, 0));
        processChunk(chunk);
    }
}

8.2 合理使用微任务和宏任务

javascript 复制代码
// 紧急任务使用微任务
function urgentTask() {
    Promise.resolve().then(() => {
        // 需要立即执行的任务
        updateUI();
    });
}

// 非紧急任务使用宏任务  
function nonUrgentTask() {
    setTimeout(() => {
        // 可以延迟执行的任务
        logAnalytics();
    }, 0);
}

9. 调试技巧

9.1 查看任务队列

javascript 复制代码
// 添加调试信息
let microtaskCount = 0;
let macrotaskCount = 0;

// 包装 Promise 来跟踪微任务
const originalThen = Promise.prototype.then;
Promise.prototype.then = function(...args) {
    microtaskCount++;
    console.log(`微任务创建,总数: ${microtaskCount}`);
    return originalThen.apply(this, args);
};

// 跟踪宏任务
const originalSetTimeout = window.setTimeout;
window.setTimeout = function(...args) {
    macrotaskCount++;
    console.log(`宏任务创建,总数: ${macrotaskCount}`);
    return originalSetTimeout.apply(this, args);
};

10. 总结

事件循环要点:

  1. 同步代码优先:先执行完所有同步任务
  2. 微任务优先:微任务在宏任务之前执行
  3. 队列清空:每个宏任务执行后都会清空微任务队列
  4. 循环不断:事件循环持续检查新任务

记忆口诀:

"同微宏,微先走,宏之后,微清空"

相关推荐
qq_334060212 小时前
SpringMVC-数据绑定(日期型)-JSR-303 Validation验证-json处理
java·开发语言·spring
SuperherRo2 小时前
JS逆向-Sign签名&绕过技术&算法可逆&替换库模拟发包&堆栈定位&特征搜索&安全影响
javascript·签名·sign
希希不嘻嘻~傻希希2 小时前
告别随意改属性!用 ES6 Class 实现数据封装
前端·javascript
Eiceblue2 小时前
使用 C# 操作 Excel 工作表:添加、删除、复制、移动、重命名
服务器·开发语言·c#·excel
娶不到胡一菲的汪大东3 小时前
C#第五讲 函数的用法
开发语言·c#
05Nuyoah3 小时前
Day 02 HTML的基础
前端·javascript·css·html·firefox·jquery·html5
小红帽6153 小时前
HTML,CSS,JS三者的功能及联系
javascript·css·html
MetaverseMan3 小时前
golang和rust内存分配策略
开发语言·golang·rust
再睡亿分钟!3 小时前
思考:客户端负载均衡和服务器负载均衡有什么区别?
java·开发语言·微服务·负载均衡