什么是事件循环

事件循环 (Event Loop) 是 JavaScript 运行时环境的核心机制,它负责协调代码执行、事件处理和渲染等操作。尽管 JavaScript 是单线程语言,但通过事件循环机制,它能够实现非阻塞的异步操作。

核心概念

  • JavaScript 引擎是单线程的

  • 事件循环使得异步操作成为可能

  • 通过任务队列管理待执行的代码

  • 区分宏任务和微任务的执行优先级

JavaScript 单线程模型

为什么是单线程?

  1. 简化编程模型:避免多线程的复杂性和竞态条件

  2. DOM 操作安全:防止多线程同时操作 DOM 造成冲突

  3. 内存管理简单:单线程环境下的垃圾回收更容易实现

事件循环的组成部分

1. 执行栈 (Call Stack)

特点:

  • LIFO (后进先出) 结构

  • 存储函数调用信息

  • 栈为空时,事件循环才会处理任务队列

2. 堆内存 (Heap)

  • 存储对象和变量

  • 动态分配内存空间

  • 垃圾回收器管理

3. 任务队列 (Task Queue)

分为两种类型:

  • 宏任务队列 (Macro Task Queue)

  • 微任务队列 (Micro Task Queue)


事件循环工作原理

完整流程图

执行顺序

  1. 执行同步代码:直接在执行栈中运行

  2. 清空微任务队列:执行所有微任务

  3. 执行一个宏任务:从宏任务队列取出一个任务

  4. 重复步骤 2-3:直到所有任务完成

详细步骤说明

// 示例代码执行流程
console.log('1'); // 同步代码,立即执行

setTimeout(() => {
    console.log('2'); // 宏任务,加入宏任务队列
}, 0);

Promise.resolve().then(() => {
    console.log('3'); // 微任务,加入微任务队列
});

console.log('4'); // 同步代码,立即执行

// 输出顺序:1, 4, 3, 2

执行分析:

  1. 执行 console.log('1') → 输出 "1"

  2. setTimeout 回调加入宏任务队列

  3. Promise.then 回调加入微任务队列

  4. 执行 console.log('4') → 输出 "4"

  5. 执行栈清空,处理微任务队列 → 输出 "3"

  6. 处理宏任务队列 → 输出 "2"

宏任务与微任务

对比表格

特性宏任务 (Macro Task)微任务 (Micro Task)
执行时机每轮事件循环执行一个每轮事件循环执行所有
优先级
常见类型setTimeout, setInterval, DOM事件Promise.then, async/await
队列清空执行一个后检查微任务必须全部执行完

宏任务 (Macro Tasks)

// 常见的宏任务
setTimeout(() => console.log('setTimeout'), 0);
setInterval(() => console.log('setInterval'), 1000);

// DOM 事件
document.addEventListener('click', () => {
    console.log('click event');
});

// HTTP 请求
fetch('/api/data').then(response => {
    // 这个 then 是微任务
    console.log('fetch response');
});

微任务 (Micro Tasks)

// Promise 相关
Promise.resolve().then(() => console.log('Promise.then'));

// async/await
async function asyncFunc() {
    await Promise.resolve();
    console.log('async/await');
}

// MutationObserver
const observer = new MutationObserver(() => {
    console.log('DOM mutation');
});

// queueMicrotask (现代浏览器)
queueMicrotask(() => console.log('queueMicrotask'));

执行优先级示例

console.log('=== 开始 ===');

// 宏任务
setTimeout(() => console.log('宏任务 1'), 0);
setTimeout(() => console.log('宏任务 2'), 0);

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

// 同步代码
console.log('同步代码');

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

/* 输出顺序:
=== 开始 ===
同步代码
=== 结束 ===
微任务 1
微任务 2
宏任务 1
宏任务 2
*/

代码示例分析

示例 1:基础执行顺序

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

example1();
// 输出:1, 4, 3, 2

分析过程:

执行栈: [example1]
1. console.log('1') → 输出 "1"
2. setTimeout 回调 → 宏任务队列: [() => console.log('2')]
3. Promise.then 回调 → 微任务队列: [() => console.log('3')]
4. console.log('4') → 输出 "4"

执行栈: []
5. 处理微任务队列 → 输出 "3"
6. 处理宏任务队列 → 输出 "2"

示例 2:嵌套异步操作

function example2() {
    console.log('开始');
    
    setTimeout(() => {
        console.log('宏任务 1');
        Promise.resolve().then(() => console.log('宏任务中的微任务'));
    }, 0);
    
    Promise.resolve().then(() => {
        console.log('微任务 1');
        setTimeout(() => console.log('微任务中的宏任务'), 0);
    });
    
    console.log('结束');
}

example2();
/* 输出:
开始
结束
微任务 1
宏任务 1
宏任务中的微任务
微任务中的宏任务
*/

示例 3:复杂的 async/await

async function example3() {
    console.log('async 开始');
    
    await Promise.resolve();
    console.log('await 后');
    
    setTimeout(() => console.log('async 中的宏任务'), 0);
    
    return 'async 返回值';
}

console.log('全局开始');

example3().then(result => {
    console.log('async 结果:', result);
});

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

console.log('全局结束');

/* 输出:
全局开始
async 开始
全局结束
await 后
全局微任务
async 结果: async 返回值
async 中的宏任务
*/

常见面试题

题目 1:经典执行顺序

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    });
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5');
    });
});

process.nextTick(function() {
    console.log('6');
});

new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8');
});

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    });
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12');
    });
});

// Node.js 环境输出:1, 7, 6, 8, 2, 4, 3, 5, 9, 11, 10, 12

题目 2:Promise 链式调用

Promise.resolve().then(() => {
    console.log('Promise1');
    setTimeout(() => console.log('setTimeout1'), 0);
}).then(() => {
    console.log('Promise2');
});

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

// 输出:Promise1, Promise2, setTimeout2, Promise3, setTimeout1

题目 3:async/await 陷阱

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
*/

最佳实践

1. 避免阻塞主线程

// ❌ 错误:长时间运行的同步代码
function badExample() {
    for (let i = 0; i < 1000000000; i++) {
        // 大量计算,阻塞主线程
    }
}

// ✅ 正确:分批处理
function goodExample() {
    let i = 0;
    function processChunk() {
        let count = 0;
        while (count < 100000 && i < 1000000000) {
            // 处理一小批数据
            i++;
            count++;
        }
        
        if (i < 1000000000) {
            setTimeout(processChunk, 0); // 让出控制权
        }
    }
    processChunk();
}

2. 合理使用微任务

// ✅ 使用微任务进行状态同步
function updateState(newState) {
    this.state = newState;
    
    // 使用微任务确保状态更新后立即执行
    Promise.resolve().then(() => {
        this.notifyObservers();
    });
}

// ✅ 使用 queueMicrotask
function scheduleUpdate() {
    queueMicrotask(() => {
        // 在下一个微任务中执行
        performUpdate();
    });
}

3. 错误处理

// ✅ 正确的异步错误处理
async function handleAsyncOperation() {
    try {
        const result = await fetchData();
        return result;
    } catch (error) {
        console.error('异步操作失败:', error);
        throw error;
    }
}

// ✅ Promise 错误处理
Promise.resolve()
    .then(() => riskyOperation())
    .catch(error => {
        console.error('Promise 链中的错误:', error);
    });

4. 性能优化

// ✅ 批量 DOM 操作
function batchDOMUpdates() {
    // 使用 requestAnimationFrame 进行批量更新
    requestAnimationFrame(() => {
        // 批量执行 DOM 操作
        elements.forEach(el => {
            el.style.transform = 'translateX(100px)';
        });
    });
}

// ✅ 防抖和节流
function debounce(func, delay) {
    let timeoutId;
    return function(...args) {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => func.apply(this, args), delay);
    };
}

总结

关键要点

  1. JavaScript 是单线程的,但通过事件循环实现异步

  2. 微任务优先级高于宏任务

  3. 每轮事件循环:执行所有微任务 → 执行一个宏任务

  4. 理解执行顺序对于编写可预测的异步代码至关重要

记忆口诀

同步代码先执行完,
微任务队列全清空,
宏任务一个一个来,
循环往复不停歇。

调试技巧

  • 使用 console.log 跟踪执行顺序

  • 利用浏览器开发者工具的 Performance 面板

  • 理解 Call Stack 和 Task Queue 的状态变化

通过深入理解事件循环机制,你将能够:

  • 编写更高效的异步代码

  • 避免常见的异步编程陷阱

  • 优化应用程序性能

  • 在面试中自信地回答相关问题

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐