JavaScript中的事件循环机制解读

 更新时间:2025年10月28日 16:45:46   作者:x-z-y  
本文全面介绍了JavaScript的事件循环机制,包括其基本概念、组成部分、任务队列的详细解释、完整的执行流程、实际代码演示、浏览器与Node.js的区别、常见面试题分析以及性能优化技巧,通过记忆口诀“同微宏,微先走,宏之后,微清空”,帮助读者更好地理解和应用事件循环机制

1. 事件循环基本概念

1.1 什么是事件循环?

事件循环是 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 是单线程的,如果没有事件循环,遇到耗时任务时页面就会卡死。

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

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

2. 事件循环的组成部分

2.1 三大核心组件

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

2.2 调用栈 (Call Stack)

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)

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 微任务有哪些?

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

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

3.3 宏任务有哪些?

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

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

4. 完整的事件循环流程

4.1 详细执行步骤

// 步骤演示
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 复杂示例分析

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 嵌套任务执行顺序

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 浏览器事件循环

// 浏览器中的阶段:
// 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 事件循环

// 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 经典面试题

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 进阶面试题

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 避免阻塞事件循环

// 错误示范:同步耗时操作阻塞事件循环
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 合理使用微任务和宏任务

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

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

9. 调试技巧

9.1 查看任务队列

// 添加调试信息
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);
};

总结

事件循环要点:

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

记忆口诀:

“同微宏,微先走,宏之后,微清空”

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • JavaScript数组排序的六种常见算法总结

    JavaScript数组排序的六种常见算法总结

    这篇文章主要给大家介绍了关于JavaScript数组排序的六种常见算法,文中通过示例代码介绍的非常详细,对大家的学习或者使用JavaScript数组具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2020-08-08
  • js 如何删除对象里的某个属性

    js 如何删除对象里的某个属性

    这篇文章主要介绍了js 如何删除对象里的某个属性,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-09-09
  • 使用console进行性能测试

    使用console进行性能测试

    各大浏览器内置的开发工具,都提供了一个console对象。它主要有两个作用:显示网页代码运行时的错误信息。提供了一个命令行接口,用来与网页代码互动。下面我们就来详细研究下如何使用console进行性能测试。
    2015-04-04
  • 灵活使用数组制作图片切换js实现

    灵活使用数组制作图片切换js实现

    这篇文章主要介绍了灵活使用数组制作图片切换效果,js实现图片切换特效,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-07-07
  • JavaScript对象与数组的几种常见复制方法

    JavaScript对象与数组的几种常见复制方法

    在 JavaScript 开发中,对象和数组的复制是一个非常常见的操作,无论是函数参数传递、状态管理,还是避免不必要的副作用,了解和掌握各种复制方式至关重要,我们将系统梳理 JavaScript 中的几种常见复制方法,并对其应用场景进行详细说明,需要的朋友可以参考下
    2024-10-10
  • 收集的一些Array及String原型对象的扩展实现代码

    收集的一些Array及String原型对象的扩展实现代码

    收集的一些Array及String原型对象的扩展实现代码,学习js的朋友可以参考下。并可以自定义的对字符串与array数据,进行扩展。
    2010-12-12
  • javascript利用apply和arguments复用方法

    javascript利用apply和arguments复用方法

    这篇文章主要介绍了javascript利用apply和arguments复用方法,有需要的朋友可以参考一下
    2013-11-11
  • 基于Proxy的小程序状态管理实现

    基于Proxy的小程序状态管理实现

    这篇文章主要介绍了基于Proxy的小程序状态管理实现,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-06-06
  • JS实现数组的增删改查操作示例

    JS实现数组的增删改查操作示例

    这篇文章主要介绍了JS实现数组的增删改查操作,结合实例形式分析了javascript针对数组的追加、获取、删除、添加、修改等常见操作技巧与相关注意事项,需要的朋友可以参考下
    2018-08-08
  • 详解template标签用法(含vue中的用法总结)

    详解template标签用法(含vue中的用法总结)

    这篇文章主要介绍了template标签用法(含vue中的用法总结),本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2021-01-01

最新评论