JS 单线程为什么不卡?一文吃透同步异步、Event Loop 和 Promise

深入浅出 JavaScript 同步异步与 Promise --- 从小白到上手

前言

写 JS 的同学一定见过这种"反直觉"的现象:明明 setTimeout 写在最前面,结果却是最后才输出。这背后就是 JavaScript 最核心的执行机制------同步与异步

本文带你从"单线程为什么还能不卡"这个问题出发,一步步搞懂 Event Loop、Promise,以及如何优雅地控制异步流程。


一、JS 为什么是单线程?

先看最简单的同步代码:

ini 复制代码
let a = 1;
let b = 2;
let c = 3;
console.log(a + b + c); // 6

一行执行完才执行下一行,这就是同步(Sync) ------顺序执行,毫无意外。

那为什么 JS 不搞多线程?C++、Java 多线程执行效率高,但编程模型复杂。JS 最初设计出来是为了在浏览器里操作 DOM、响应用户交互,如果多个线程同时操作同一个 DOM 节点,到底听谁的?所以 JS 干脆选择了单进程单线程,够简单、够可靠。

类比理解:

  • 进程 = 董事长(有 PID),负责分配公司资源
  • 线程 = 经理,实际干活的人
  • JS 这家公司只有一个董事长 + 一个经理,事情一件一件来,不会乱也不会打架

二、遇到耗时任务怎么办?------ Event Loop(事件循环)

单线程最大的问题就是:遇到耗时任务(定时器、网络请求)怎么办?难道整个页面卡住不动?

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

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

console.log('end');

// 输出顺序:start → end → 222

222 明明写在前面,却是最后输出。因为 JS 的执行策略是:

  1. 先把同步代码执行完 --- 同步代码是用户马上需要看到的页面内容,不能拖
  2. 遇到 setTimeoutfetch、事件监听等耗时性任务(Async Task) ,JS 会把它们丢进 Event Loop(事件循环) ,暂时跳过
  3. 同步代码全部跑完后,再到 Event Loop 里把异步任务取出来执行

核心知识点: CPU 的执行时间是按几十毫秒的轮询分配给进程的。JS 不能让一个耗时任务霸占主线程不放,所以用 Event Loop 把异步任务"排队",同步清空后再回头处理。

画个图帮你记住:

arduino 复制代码
调用栈(同步代码一条道走到黑)
    ↓ 遇到异步 → 丢进任务队列
任务队列(setTimeout、fetch、事件...)
    ↓ 等同步代码全跑完
拿出来一个一个执行

三、异步顺序怎么控制?

真实开发中常有这种需求:

A. fetch 获取所有用户列表 → B. 根据结果再 fetch 每个用户详情

B 依赖 A 的结果,这种"串行异步"如果只用回调函数写,就会陷入回调地狱

javascript 复制代码
// 反模式,别这么写
getUsers(function(users) {
    getUserDetail(users[0].id, function(detail) {
        getOrders(detail.id, function(orders) {
            // 一层套一层,越套越深...
        });
    });
});

所以 ES6 带来了 Promise,专门解决异步流程控制。


四、Promise --- 给异步任务一个"承诺"

4.1 Promise 是什么?

把 Promise 理解成一张承诺书

  • 你说"2 秒后给你结果" → 这就是创建了一个 Promise
  • 2 秒后成功了 → 履约(resolve) ,走 .then()
  • 2 秒后失败了 → 毁约(reject) ,走 .catch()
  • 不管成败 → 最后都会走 .finally()
javascript 复制代码
const p = new Promise((resolve, reject) => {
    console.log('许诺言');  // executor 立即执行!这是同步代码

    // 耗时性任务,2 秒后出结果
    setTimeout(() => {
        // resolve(666);         // 成功 → then
        reject('网络错误');       // 失败 → catch
    }, 2000);
});

console.log(p.__proto__);  // then/catch/finally 都在原型上

p
    .then(data => {
        console.log(data);   // resolve 传的值会到这里
    })
    .catch(error => {
        console.log(error);  // reject 传的值会到这里
    })
    .finally(() => {
        console.log('finally');  // 不管成败都会执行
    });

4.2 Promise 核心要点

要点 说明
executor 立即执行 new Promise(fn) 里的 fn同步的,Promise 只是容纳异步任务的"容器"
resolve 表示异步任务成功 → 触发 .then()
reject 表示异步任务失败 → 触发 .catch()
状态不可逆 一旦 resolve 或 reject,状态就固定了,不能反悔
链式调用 .then().catch().finally() 像流水线一样处理结果

4.3 fetch --- Promise 的最佳实践

fetch 是浏览器内置 API,底层返回的就是 Promise:

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

fetch('https://api.deepseek.com/chat/completions', {
    method: 'POST',
})
    .then(data => {
        console.log(data);  // 响应成功时处理
    })
    .catch(error => {
        console.log(error); // 网络出错时处理
    });

console.log('end');

// 输出:start → end → data(data 是异步的,最后到)

4.4 手写 sleep 函数,彻底搞懂 Promise

JS 没有像 Python 那样的 time.sleep(),但我们可以用 Promise 自己封装:

javascript 复制代码
function sleep(t) {
    const p = new Promise((resolve, reject) => {
        console.log('同步');        // executor 立即执行!
        setTimeout(() => {
            resolve();              // t 毫秒后履约
        }, t);
    });
    return p;                       // 返回 Promise,让外面 .then()
}

sleep(2000).then(() => {
    console.log('2000ms 后执行');
});

// 输出:
// 同步            ← 立即输出
// 2000ms 后执行    ← 2 秒后输出

这个 sleep 完美展示了 Promise 的套路:

  1. new Promise 包裹异步操作
  2. executor 里的同步代码立即执行console.log('同步')
  3. 异步任务完成 → 调用 resolve
  4. 返回 Promise,外部用 .then() 拿到结果

五、总结

概念 一句话
同步 上一行跑完再跑下一行
异步 先跳过,同步跑完再回来处理
Event Loop JS 的"调度中心",把异步任务排队,同步清空后再取
Promise 异步任务的"承诺书",resolve → then,reject → catch
executor new Promise(fn) 里的 fn,同步立即执行,内部包裹异步任务

最后一句话记住 Promise:

Promise 是容纳异步任务的容器,executor 同步立即执行;成功调 resolve → 走 then,失败调 reject → 走 catch。


六、来挑战一下

下面代码的输出顺序是什么?(答案在最后)

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

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

const p = new Promise((resolve, reject) => {
    console.log('3');
    resolve('4');
});

p.then(data => {
    console.log(data);
});

console.log('5');

答案:1 → 3 → 5 → 4 → 2

解析:

  1. console.log('1') --- 同步,直接输出 1
  2. setTimeout --- 异步,丢进宏任务队列,跳过
  3. Promise executor --- 同步立即执行 ,输出 3resolve('4').then 丢进微任务队列
  4. console.log('5') --- 同步,输出 5
  5. 同步跑完 → 先清空微任务 (Promise.then)→ 输出 4
  6. 再处理宏任务 (setTimeout)→ 输出 2

如果你能完整解释这个输出顺序,恭喜,JS 同步异步机制已经拿下了!🎉

相关推荐
葬送的代码人生1 小时前
JavaScript 数组完全指南:从入门到实战
前端·javascript·算法
用户938515635072 小时前
深入理解 JavaScript 同步与异步:从单线程到事件循环与 Promise
前端·javascript
哈撒Ki2 小时前
快速入门vue3与常见面试题
前端·vue.js·面试
2301_800895102 小时前
线性代数保研面试复习
线性代数·面试·保研
云水一下3 小时前
Vue.js从零到精通系列(一):初识Vue——背景、环境与第一个应用
前端·javascript·vue.js
大大杰哥3 小时前
Vue2学习(1)--了解基本方法与概念
javascript·学习·vue
白露与泡影3 小时前
2026秋招冲刺:1000道Java高频面试题(各大厂考点汇总)
java·开发语言·面试
云水一下3 小时前
Vue.js从零到精通系列(二):响应式核心——ref、reactive、computed与watch
前端·javascript·vue.js
Cosolar3 小时前
深入理解 LangChain Callback 机制:从入门到实战
人工智能·后端·面试