← JavaScript setInterval() JavaScript Promise →

JavaScript事件循环

原创 2026-03-14 JavaScript 已有人查阅

JavaScript事件循环:单线程如何搞定异步任务

不少同学刚接触JavaScript时都会困惑:明明JavaScript是单线程语言,为什么还能处理网络请求、定时器这些异步操作?页面加载时不会卡死吗?这背后起作用的就是事件循环(Event Loop)机制。

事件循环是什么?

事件循环是JavaScript管理代码执行的机制,特别是处理那些异步任务——比如定时器回调、接口请求、Promise等。JavaScript只有一个主线程,要同时处理多个任务还不阻塞,就得靠事件循环来调度。

拿我平时调试代码的体会来说,如果没搞懂事件循环,写出来的代码执行顺序往往会出乎意料。看个基础例子:

console.log("开始学习");  
setTimeout(() => {  
    console.log("定时器回调执行");  
}, 0);  
Promise.resolve().then(() => {  
    console.log("Promise完成");  
});  
console.log("学习结束");  

运行结果:

开始学习
学习结束
Promise完成
定时器回调执行

看到这个结果,初学者会纳闷:setTimeout不是设置0毫秒吗?为什么Promise反而先执行了?

事件循环的两大核心组件

1. 调用栈(Call Stack)

调用栈采用后进先出(LIFO,Last in,First out)原则,记录着当前正在执行的函数。函数调用时入栈,执行完毕就出栈。栈空了,主线程就空闲下来准备处理他任务。

2. 任务队列

实际上任务队列分两种,优先级不同:

  • 微任务队列(Microtask Queue):存放Promise.then、catch、finally、queueMicrotask等。优先级高,每次调用栈清空后立刻执行。

  • 宏任务队列(Macrotask Queue):也叫回调队列,存放setTimeout、setInterval、I/O操作、UI渲染等。优先级低,等微任务处理完才轮到。

事件循环的工作流程

事件循环就像个勤奋的监工,按照特定顺序反复检查:

  1. 先执行调用栈里的同步代码

  2. 调用栈清空后,立即处理微任务队列(全部执行完)

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

  4. 重复以上步骤

我用实际项目经验告诉你,这种设计保证了高优先级任务(比如Promise回调)能尽快执行,而不会因为setTimeout这类任务堵在后面。

深入理解:微任务vs宏任务

看这个对比代码,执行顺序很清楚:

console.log("代码号学习: 开始");  

setTimeout(() => {  
    console.log("宏任务: setTimeout");  
}, 0);  

Promise.resolve()
    .then(() => {  
        console.log("微任务: Promise第一个then");  
    })
    .then(() => {
        console.log("微任务: Promise链式第二个then");  
    });

console.log("代码号学习: 结束");  

输出:

代码号学习: 开始
代码号学习: 结束
微任务: Promise第一个then
微任务: Promise链式第二个then
宏任务: setTimeout

个人见解:项目开发时,如果你想让某段代码尽快执行,用微任务(比如Promise)比setTimeout(callback, 0)更靠谱。我遇到过好几次用setTimeout(0)模拟异步,结果顺序不符合预期,就是因为忽略了微任务的优先级。

实际应用场景

场景1:防止页面卡顿

假设要处理大量数据,直接在主线程执行会阻塞用户操作:

// 错误写法 - 大数据处理导致页面假死
function processLargeData() {
    for(let i = 0; i < 100000; i++) {
        // 复杂计算
    }
    updateUI();
}

// 正确写法 - 拆分任务,让事件循环有机会处理用户交互
function processLargeDataChunk() {
    let i = 0;
    function doChunk() {
        // 每次处理一小部分
        for(let j = 0; j < 100; j++, i++) {
            // 部分计算
        }
        if(i < 100000) {
            setTimeout(doChunk, 0); // 让出主线程
        } else {
            updateUI();
        }
    }
    doChunk();
}

场景2:多个异步任务顺序控制

// 需求:先显示加载状态,再请求数据,之后更新界面
function loadData() {
    // 立即显示加载
    showLoading();  
    
    // 微任务:确保在DOM更新前处理
    Promise.resolve().then(() => {
        console.log("准备请求数据");
    });
    
    // 模拟API请求
    setTimeout(() => {
        const data = fetchData();
        // 使用微任务更新UI,避免阻塞渲染
        Promise.resolve().then(() => {
            updateUI(data);
            hideLoading();
        });
    }, 1000);
}

Node.js环境的事件循环

Node.js的事件循环和浏览器略有不同,分为6个阶段:

  1. timers(定时器阶段):执行setTimeout和setInterval的回调

  2. pending callbacks(待处理回调):执行延迟到下一轮的I/O回调

  3. idle, prepare(闲置/准备):仅Node内部使用

  4. poll(轮询阶段):获取新I/O事件,执行I/O回调

  5. check(检查阶段):执行setImmediate回调

  6. close callbacks(关闭回调):处理socket.on('close')这类关闭事件

个人经验:Node开发中,想立即执行异步代码,setImmediate()比setTimeout(callback, 0)更高效。因为setImmediate在check阶段执行,而setTimeout还要经过timers阶段的时间检查。

事件循环的重要性

  1. 非阻塞I/O:JavaScript可以同时处理文件读写、网络请求,不阻塞主线程

  2. 响应式界面:用户点击、滚动等操作能及时响应,不会被繁重的计算任务卡住

  3. 单线程下的并发:虽然是单线程,通过事件循环实现了类似多线程的效果

知识点

  • 调用栈:同步代码的执行场所

  • 微任务队列:Promise、MutationObserver、queueMicrotask,优先级高

  • 宏任务队列:setTimeout、setInterval、I/O、UI渲染、setImmediate(Node)

  • 事件循环规则:先同步,再微任务,后宏任务;宏任务取一个,清空微任务,循环往复

之后分享个调试技巧:搞不清执行顺序时,可以在代码里加console.log打点,观察输出顺序。事件循环这个概念,光看文档容易晕,多写几个例子就能摸清规律了。

← JavaScript setInterval() JavaScript Promise →
分享笔记 (共有 篇笔记)
验证码:
微信公众号