JS 同步与异步:从单线程到 Promise

JS 是单线程语言,但前端要处理的定时器、点击事件、网络请求都是耗时的。怎么不卡死页面还能把这些事做完?答案是:Event Loop + Promise


一、JS 为什么是单线程

1. 进程与线程

概念 类比 说明
进程 董事长 操作系统分配资源的基本单位,有独立 PID
线程 经理 进程内执行代码的基本单位,一个进程可有多个线程
主线程 总经理 进程启动时自动创建的线程
子线程 副经理 主线程启动的其他线程

CPU 执行时间 :不能被一个程序霸占,操作系统用几十毫秒的轮询给每个进程分配执行时间片,看起来像多个程序同时跑。

2. 多线程 vs 单线程

语言 架构 特点
C++ / Java 多进程多线程 执行效率高,但复杂(锁、竞态、死锁)
JavaScript 单线程 简单安全(操作 DOM 不会冲突),但处理耗时任务有挑战

JS 单线程的原因:最初设计用于浏览器操作 DOM,如果多线程同时改 DOM 会乱套。简单是它的设计哲学。

3. JS 的异步任务有哪些

单线程意味着一次只能做一件事,但下面这些任务都很耗时:

  • setTimeout 定时器
  • 用户点击事件
  • fetch / axios 网络请求
  • 文件读取、数据库查询(Node/Bun 场景)

如果用同步方式处理:等定时器跑完、等请求返回,整个页面就卡死了------按钮点不动、动画不动、页面假死。

怎么办? → 异步机制。


二、JS 执行机制:Event Loop

1. 进程和线程的启动

bash 复制代码
前端 script 或后端 node/bun 代码执行
       ↓
启动一个进程(独立 PID,负责分配资源)
       ↓
进程启动一个主线程(JS 单线程,所有代码在这跑)

2. 同步优先,异步靠后

JS 的执行策略是:先把同步代码快速跑完,把耗时任务扔到 event loop,等主线程空了再回来处理

javascript 复制代码
console.log('start');           ← 1. 同步,立即执行
setTimeout(() => {
    console.log('222');         ← 3. 异步,1 秒后执行
}, 1000);
console.log('end');             ← 2. 同步,立即执行

输出顺序start → end → 222

3. Event Loop 工作流程

vbnet 复制代码
┌─────────────────────────────────────────┐
│  主线程(同步代码)                       │
│  ┌───────────────────────────────────┐  │
│  │ 1. console.log('start')          │  │
│  │ 2. setTimeout → 扔进 Event Loop   │  │ ← 跳过,先往下执行
│  │ 3. console.log('end')            │  │
│  │ 4. fetch → 扔进 Event Loop        │  │ ← 跳过
│  │ 5. 同步代码全部执行完...           │  │
│  └───────────────────────────────────┘  │
│                  ↓ 主线程空闲             │
└──────────────────┼──────────────────────┘
                   ↓
┌──────────────────────────────────────────┐
│  Event Loop(任务队列)                  │
│  - 检查定时器到没到时间                  │
│  - 检查网络请求返回没                    │
│  - 检查用户事件触发没                    │
│  → 满足条件的回调,推回主线程执行         │
└──────────────────────────────────────────┘

核心 :JS 不是真的"同时"做很多事,而是把等待的活先挂着,先做能立刻完成的,等闲下来了再回来处理挂着的。这样页面就不会卡死,用户看到的同步代码(页面渲染、按钮响应)能快速展示。


三、为什么需要控制执行流程

异步虽然不卡死,但带来新问题:顺序不可控

经典场景:先拿用户列表,再拿每个用户的详情

js 复制代码
// A: 先 fetch 所有用户
fetch('/api/users')
    .then(users => {
        // B: 再 fetch 每个用户
        // 如果 A 没完成,users 是 undefined,B 会报错
    });

如果 A、B 都是异步的,且 B 依赖 A 的结果,就必须确保 A 先完成。这就是"控制执行流程"的需求。

JS 历史上有三种方案:

  1. 回调函数(callback hell,回调地狱)
  2. Promise(ES6,链式调用,本笔记重点)
  3. async/await(ES7,语法糖,最直观)

四、理解 Promise

1. 核心概念:一个"承诺"

Promise 直译就是"承诺"。你可以把它理解成一个未来会有结果的盒子

  • 现在盒子是空的 → pending(进行中)
  • 将来兑现了 → fulfilled(成功)
  • 将来毁约了 → rejected(失败)

2. 实例化 Promise

js 复制代码
const p = new Promise((resolve, reject) => {
    // 这个函数叫 executor(执行器)
    // 会立即执行,是耗时性任务的容器
    
    console.log('许诺言');      // 同步,立即执行
    
    setTimeout(() => {
        // resolve 和 reject 是 Promise 给的两个函数
        resolve(666);           // 成功 → 调用 resolve
        // reject("网络错误");  // 失败 → 调用 reject
    }, 2000);
});

关键点

概念 说明
executor 函数 传给 new Promise 的函数,立即同步执行
容器作用 executor 里可以放异步任务(如 setTimeout)
resolve 函数 表示"成功解决了",手动调用
reject 函数 表示"失败了",手动调用

3. 三个状态 + 三个回调

js 复制代码
p
    .then((data) => {       // resolve 了走 then,data 是 resolve 传的参数
        console.log(data);  // 666
    })
    .catch((err) => {       // reject 了走 catch,err 是 reject 传的参数
        console.log(err);   // "网络错误"
    })
    .finally(() => {        // 无论成功失败,都会执行
        console.log('finally');
    });

4. 数据传递流程

scss 复制代码
resolve(result)
      ↓
      └→ .then(data => { ... })    // data = result

reject(err)
      ↓
      └→ .catch(error => { ... })  // error = err

关键resolve(666) 里的 666,会作为 .then(data => ...)data 的值;reject("网络错误") 里的字符串,会作为 .catch(err => ...)err 的值。


五、实战代码拆解

1. 同步 vs 异步执行顺序

js 复制代码
console.log('start');              // 同步
setTimeout(() => {                 // 异步
    console.log('222');
}, 1000);
console.log('end');                // 同步

// 输出:start → end → 222

验证 Event Loop :setTimeout 被扔进任务队列,主线程先跑完 end,1 秒后定时器触发,才把 222 推回主线程执行。

2.多线程假想

js 复制代码
// 如果是多线程,可以 3 个线程分别声明 a, b, c,并发执行
// 缺点是复杂(要处理线程同步、锁)
let a = 1;    // 单线程顺序执行
let b = 2;
let c = 3;
console.log(a + b + c);   // 6

对比说明:单线程代码简单直接,但处理耗时任务要靠异步机制。

3. Promise 完整流程

js 复制代码
const p = new Promise((resolve, reject) => {
    console.log('许诺言');          // 1. 立即同步执行
    setTimeout(() => {
        // resolve(666);           // 成功路径
        reject("网络错误");         // 失败路径(演示)
    }, 2000);
});

console.log(p.__proto__);          // 2. 同步,看 Promise 的原型
p
    .then((data) => {              // 3. 成功时执行
        console.log(data);
        console.log('end');
    })
    .catch((err) => {              // 4. 失败时执行
        console.log(err);          // "网络错误"
        console.log('失败了');
    })
    .finally(() => {               // 5. 无论成功失败都执行
        console.log('finally');
    });

输出(reject 路径):

javascript 复制代码
许诺言              ← 立即
Promise { ... }     ← 立即(原型对象)
(等 2 秒)
网络错误            ← 2 秒后
失败了
finally

4. fetch 底层就是 Promise

js 复制代码
console.log('start');
fetch('https://api.github.com/chat/completions', {
    method: 'POST'
})
    .then((data) => { /* 请求成功 */ })
    .catch((err) => { console.log(err); });
console.log('end');

// 输出顺序:start → end → (请求返回后) then 或 catch

说明fetch 本身返回一个 Promise,所以能直接 .then().catch()。所有现代异步 API(fetch、axios、数据库驱动)都是基于 Promise 的。

5. 用 Promise 封装 sleep

js 复制代码
function sleep(t) {
    const p = new Promise((resolve, reject) => {
        console.log('同步');    // 调用 sleep 时立刻打印
        setTimeout(() => {
            resolve();         // 这个回调才是异步,t 毫秒后才执行,表示"承诺兑现"
        }, t);
    });
    return p;                           // 返回 Promise,
}

sleep(2000)
    .then(() => {
        console.log('2s 后再做');
    });

关键点 1: new Promise(...) 里的代码是立即同步执行 的,Promise 构造函数传进去的那个函数,会当场执,不是异步。所以:

  • console.log('同步') → 立即 执行
  • setTimeout(..., t) → 启动 定时器(立即返回),回调要 t 毫秒后才跑
  • resolve() → t 毫秒后定时器触发时才调用,表示"承诺兑现"

关键点 2: return p 的作用: sleep 函数返回这个 Promise,让外面能 .then() 它。

为什么 JS 不内置 sleep:单线程同步 sleep 会卡死整个页面 2 秒。用 Promise 包 setTimeout,是"模拟延迟"的标准做法,不会阻塞主线程。


六、三种写法对比(同一件事)

写法 代码 特点
回调函数 sleep(2000, () => { ... }) 嵌套深,回调地狱
Promise .then() sleep(2000).then(() => { ... }) 链式调用,可读
async/await await sleep(2000); ... 像同步代码,最直观

三者关系:async/await 是 Promise 的语法糖,Promise 是回调函数的升级版

js 复制代码
// Promise 写法
sleep(2000).then(() => {
    console.log('2s 后再做');
});

// async/await 写法(等价)
async function main() {
    await sleep(2000);
    console.log('2s 后再做');
}
main();

七、Promise 的本质:状态机

kotlin 复制代码
                pending(进行中)
                  /          \
              resolve         reject
                /              \
              ↓                ↓
        fulfilled            rejected
        (已兑现)            (已毁约)
            │                    │
        .then(data)          .catch(err)
            │                    │
            └──→ .finally() ←────┘

关键特性

  1. 状态不可逆:从 pending 变成 fulfilled 或 rejected 后,就固定了,不能再变
  2. 只认第一次:executor 里无论调多少次 resolve/reject,只有第一次生效
  3. 值穿透:resolve 传的值,会一路传到 .then;reject 传的值,会一路传到 .catch
js 复制代码
const p = new Promise((resolve, reject) => {
    resolve('A');
    resolve('B');    // ❌ 无效,已经 resolve 过了
    reject('C');     // ❌ 无效
});
p.then(data => console.log(data));   // 'A'

八、总结

核心知识点

  1. JS 是单线程:简单安全,但要靠异步机制处理耗时任务
  2. Event Loop:同步代码优先执行,异步任务扔进队列,主线程空闲时再处理
  3. Promise 三状态:pending → fulfilled / rejected
  4. executor 立即同步执行new Promise(函数) 里的函数是同步的,但里面可以放异步任务
  5. 数据传递resolve(data) 的 data → .then(data)reject(err) 的 err → .catch(err)
  6. fetch / axios 底层都是 Promise:现代异步 API 的标准接口

心法

异步不是"同时做",而是"先挂着,闲了再做"。Promise 是装"未来结果"的盒子,.then 等兑现,.catch 等毁约,.finally 最后都要执行。

选型建议

  • 简单异步setTimeout + 回调够用
  • 有依赖链 :Promise .then().catch()
  • 多步异步且要顺序执行async/await(最直观)
  • 多个异步并发等全部完成Promise.all([p1, p2, p3])
  • 多个异步只要有一个完成Promise.race([p1, p2, p3])
相关推荐
sugar__salt2 小时前
深入理解 React useContext:跨层级组件通信的核心利器
前端·javascript·react.js·前端框架·框架
谷哥的小弟2 小时前
TypeScript类型断言
前端·javascript·typescript
weixin_BYSJ19872 小时前
springboot医疗信息管理系统---附源码17465
java·javascript·spring boot·python·django·flask·php
zhongjunyao3 小时前
Spec-Driven Development (SDD): The Definitive 2026 Guide
javascript·spring boot·python·eclipse·代理模式
vx-程序开发15 小时前
springboot农产品运输服务平台---附源码75498
java·javascript·spring boot·python·eclipse·django·php
用户9385156350715 小时前
React 受控/非受控组件与 React.memo 性能优化——从本质到实战
前端·javascript·react.js
其美杰布-富贵-李16 小时前
第 8 篇:Three.js 材质系统
javascript·three.js·js
To_OC16 小时前
LC 3 无重复字符的最长子串:从入门滑动窗口到优化写法,再也不怕面试官追问
javascript·算法·leetcode
默_笙17 小时前
🚩 React + TypeScript 的 Props 通信,我从"把事件对象传给父组件"进化到了"只传值"
前端·javascript