JavaScript 同步异步与 Promise 详解 —— 从 Event Loop 到手写 sleep

JavaScript 同步异步与 Promise 详解

前言

JavaScript 的异步编程是前端开发中绕不开的核心概念。从最初接触 setTimeout 时的困惑,到熟练运用 Promise 控制异步流程,这个过程需要理解 JS 的执行机制。本文将从基础概念出发,系统梳理 JS 的单线程模型、事件循环(Event Loop)、Promise 及其原型链,帮助你彻底搞懂 JS 的同步与异步。


一、JS 的单线程架构

1.1 为什么 JS 是单线程的?

JavaScript 自诞生之初就被设计为单线程语言。这与它的应用场景密切相关:JS 最初运行在浏览器中,用于处理页面交互和 DOM 操作。试想一下,如果多个线程同时操作同一个 DOM 节点,一个要删除、一个要修改,浏览器该听谁的?为了避免这种竞态条件带来的复杂性,JS 选择了单线程模型。

javascript 复制代码
// JS 是单线程语言,代码按顺序一行一行执行
let a = 1;
let b = 2;
let c = 3;
console.log(a + b + c); // 6

1.2 单线程 vs 多线程

特性 单线程 (JavaScript) 多线程 (C++/Java 等)
复杂度 简单,易于理解 复杂,需处理锁、死锁等问题
执行效率 一个任务阻塞则后续全等 多任务并发,效率更高
适用场景 I/O 密集型(网络请求、文件读写) CPU 密集型(大量计算)

补充知识 :现代 JS 也提供了多线程能力------浏览器的 Web Worker 和 Node.js 的 Worker Threads。但它们不共享内存,通过消息通信,避免了传统多线程的竞态问题。主线程依然是单线程模型。


二、同步与异步

2.1 同步代码(Sync)

同步代码按照书写顺序依次执行,前一个任务完成后才会执行后一个。

javascript 复制代码
console.log("start");
// 此处若是耗时操作,整个页面会"卡住"
console.log("end");

2.2 异步代码(Async)

当遇到耗时任务(定时器、网络请求、事件监听等),JS 不会傻等,而是将其挂起,继续执行后续同步代码。等同步代码全部执行完毕后,再回过头处理异步回调。

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

setTimeout(() => {
    console.log("延迟 1000ms 后执行");
}, 1000);

console.log("end");

// 输出顺序:
// start
// end
// 延迟 1000ms 后执行

2.3 setTimeout 的"谎言"

需要特别注意:setTimeout 的第二个参数 1000ms 并不是"1000ms 后一定执行回调",而是至少等待 1000ms 后,才能把回调放入任务队列。放入队列 ≠ 立刻执行,回调还必须满足以下条件:

  1. 调用栈清空(所有同步代码执行完)
  2. 它前面排队的其他宏任务也执行完
  3. 如果主线程正在忙碌,回调就得一直排队
javascript 复制代码
console.log("start");

setTimeout(() => {
    console.log("定时器回调");
}, 0); // 即使是 0ms,也不会立即执行!

// 模拟一个耗时同步操作(阻塞主线程 1 秒)
const start = Date.now();
while (Date.now() - start < 1000) {}

console.log("end");

// 输出顺序:start → end → 定时器回调
// 即使延迟设为 0,也要等同步代码全部执行完

三、进程与线程

理解 JS 运行环境需要先区分两个概念:

概念 比喻 说明
进程(Process) 董事长 (PID) 操作系统分配资源的最小单位。每个进程拥有独立的内存空间
线程(Thread) 经理 CPU 调度的最小单位。一个进程至少有一个主线程,也可以启动多个子线程
  • C++ / Java 等系统级语言:支持多进程、多线程架构,执行效率高,但编程复杂。
  • JavaScript:天生设计为单线程架构,简单可靠。浏览器或 Node.js 启动一个进程后,JS 在主线程上运行。

四、JS 的执行机制 ------ Event Loop

4.1 整体流程

text 复制代码
1. 启动进程 (PID),分配资源
2. 进程启动主线程
3. 主线程执行同步代码(快速渲染页面)
4. 遇到异步任务 → 交给浏览器/Node
   (其回调进入任务队列,由 Event Loop 调度)
5. 继续执行后续同步代码
6. 同步代码执行完毕 → Event Loop
   从任务队列中取出回调执行

4.2 宏任务与微任务

JS 的异步任务分为两类:

类型 常见 API 优先级
宏任务 (Macro Task) setTimeoutsetIntervalsetImmediate(Node)、I/O、UI 渲染
微任务 (Micro Task) Promise.then/catch/finallyMutationObserverqueueMicrotask

注意 :Node.js 中还有 process.nextTick,它拥有比普通微任务更高的优先级(独立于微任务队列,每轮事件循环中先于 Promise 回调执行)。
执行顺序:每轮事件循环中,先执行一个宏任务 → 然后清空所有微任务 → 必要时进行 UI 渲染 → 再取下一个宏任务,如此往复。

4.3 经典面试题

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

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

Promise.resolve().then(() => {
    console.log("3");
});

console.log("4");

// 输出顺序:1 → 4 → 3 → 2
// 解释:1 和 4 是同步代码
//       3 是微任务(Promise.then),在宏任务之前执行
//       2 是宏任务(setTimeout),最后执行

五、理解 Promise

5.1 为什么需要 Promise?

在没有 Promise 的年代,异步操作依赖回调函数,很容易陷入回调地狱(Callback Hell):

javascript 复制代码
// 回调地狱示例(伪代码)
getUser(userId, (user) => {
    getOrders(user.id, (orders) => {
        getOrderDetail(orders[0].id, (detail) => {
            // 层层嵌套,难以维护
        });
    });
});

Promise 是 ES6 引入的异步任务控制的最佳机制 ,它将异步操作以链式调用的方式组织,让代码更可读、更易维护。

5.2 Promise 的基本用法

javascript 复制代码
// 创建一个 Promise 实例
const p = new Promise((resolve, reject) => {
    // 这个箭头函数叫做 executor(执行器)
    // executor 会立即同步执行!
    console.log("许诺"); // 这行代码会同步输出

    // 放置耗时性异步任务
    setTimeout(() => {
        // resolve(666);  // 履约:把 666 传递给 then
        reject("操作失败!"); // 拒绝:把错误信息传递给 catch
    }, 2000);
});

// resolve 和 reject 本质上是 Promise 给你的两个触发器
// 你在合适时机(异步操作完成/失败)调用它们
// Promise 负责通知后续的 .then() / .catch()

p
    .then((data) => {
        // resolve 被调用时,这里执行
        console.log("成功:", data);
    })
    .catch((err) => {
        // reject 被调用时,这里执行
        console.log("失败:", err);
    })
    .finally(() => {
        // 无论成功还是失败,都会执行
        console.log("无论如何都会执行 finally");
    });

5.3 Promise 的三个状态

状态 含义 触发条件
pending 待定(初始状态) Promise 刚刚创建
fulfilled 已兑现 resolve() 被调用
rejected 已拒绝 reject() 被调用

关键特性:Promise 的状态一旦改变,就不可逆转。pending → fulfilled 或 pending → rejected 后,状态就永久固定了。

5.4 resolve 和 reject 的参数传递

  • resolve(data)data 传给 .then() 的回调 ------ 只能传递一个参数,不能传两个。如果需要多个值,可以用对象或数组包装。
  • reject(err) 将失败原因传给 .catch() 的回调。

5.5 Promise 的链式调用

javascript 复制代码
fetchUser(1)
    .then((user) => {
        console.log("用户信息:", user);
        return fetchOrders(user.id); // 返回一个新的 Promise
    })
    .then((orders) => {
        console.log("订单列
    .then((detail) => {
        console.log("订单详情:", detail);
    })
    .catch((err) => {表:", orders);
        return fetchOrderDetail(orders[0].id);
    })
    .then((detail) => {
        console.log("订单详情:", detail);
    })
    .catch((err) => {
        // 任何一个环节出错,都会进入这里
        console.log("出错了:", err);
    });

六、Fetch API ------ 基于 Promise 的网络请求

6.1 基本用法

fetch 是浏览器内置的现代网络请求 API,其底层就是 Promise

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

fetch("https://api.example.com/data", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
    },
    body: JSON.stringify({ key: "value" }),
})
    .then((response) => {
        // response 是 Response 对象,不是直接的业务数据
        // 需要根据格式调用 .json() / .text() / .blob() 等方法
        if (!response.ok) {
            throw new Error(`HTTP 错误:${response.status}`);
        }
        return response.json(); // 返回一个 Promise,解析 JSON 数据
    })
    .then((data) => {
        // 这里才是实际的业务数据
        console.log("获取到的数据:", data);
    })
    .catch((err) => {
        console.log("请求失败:", err);
    });

console.log("end1");

// 输出顺序:start → end1 → (网络请求完成后) 获取到的数据...

常见误区fetch 只有遇到网络错误 时才会 reject(如断网、DNS 解析失败)。HTTP 错误状态码(如 404、500)并不会触发 reject,需要手动检查 response.okresponse.status

6.2 async/await 写法

ES2017 引入了 async/await,让异步代码看起来像同步代码:

javascript 复制代码
async function getData() {
    try {
        const response = await fetch("https://api.example.com/data");
        if (!response.ok) {
            throw new Error(`HTTP 错误:${response.status}`);
        }
        const data = await response.json();
        console.log("数据:", data);
    } catch (err) {
        console.log("出错:", err);
    }
}

七、手写 sleep 函数

JavaScript 没有内置的 sleep() 函数(像 Python 那样),但我们可以用 Promise 封装一个:

javascript 复制代码
// JS 系统不支持 sleep 功能,自己封装
function sleep(t) {
    const p = new Promise((resolve) => {
        // executor 内的代码是同步执行的
        console.log("同步代码块(Promise 执行器立即执行)");
        setTimeout(() => {
            resolve();
        }, t);
    });
    return p;
}

// 使用
sleep(2000).then(() => {
    console.log("2000ms 后才运行这里");
});

// 或者配合 async/await
async function demo() {
    console.log("开始");
    await sleep(2000);
    console.log("2 秒后执行");
}
demo();

现在 await sleep(ms) 已经成为一种常见的延迟模式,在许多项目中都能看到类似封装。


八、Promise 的原型链

Promise 的方法分布在静态方法原型方法两个位置:

javascript 复制代码
const p = new Promise((resolve, reject) => {
    setTimeout(() => resolve(666), 2000);
});

// 原型方法(实例方法)------ 在 p.__proto__ 上
console.log(p.__proto__);
// 包含:then、catch、finally

// 静态方法 ------ 在 Promise 构造函数本身上
// Promise.resolve()     → 快速创建一个已兑现的 Promise
// Promise.reject()      → 快速创建一个已拒绝的 Promise
// Promise.all([])       → 所有 Promise 都成功才算成功
// Promise.allSettled([])→ 等所有 Promise 都完成(不管成败)
// Promise.race([])      → 第一个完成的 Promise(无论成败)
// Promise.any([])       → 第一个成功的 Promise(忽略失败)

常用静态方法示例

javascript 复制代码
// Promise.all ------ 全部成功才成功
Promise.all([fetch("/api/user"), fetch("/api/orders"), fetch("/api/products")])
    .then(([user, orders, products]) => {
        console.log("三个请求都成功了");
    })
    .catch((err) => {
        console.log("至少有一个请求失败了");
    });

// Promise.race ------ 竞速,第一个完成的为准
Promise.race([
    fetch("/api/server1"),
    fetch("/api/server2"),
    fetch("/api/server3"),
]).then((fastestResponse) => {
    console.log("最快的服务器响应了");
});

// Promise.allSettled ------ 等全部完成,不管成败
Promise.allSettled([fetch("/api/a"), fetch("/api/b")]).then((results) => {
    results.forEach((r, i) => {
        console.log(`请求 ${i}:${r.status}`); // "fulfilled" 或 "rejected"
    });
});

九、总结

概念 核心要点
同步 按顺序执行,一个接一个
异步 耗时任务不阻塞主线程,通过事件循环回调执行
Event Loop JS 的运行机制:同步 → 微任务 → 宏任务 → 渲染
Promise ES6 异步控制方案,三种状态 + then/catch/finally 链式调用
fetch 基于 Promise 的现代网络请求 API
async/await Promise 的语法糖,让异步代码形同同步

理解 JavaScript 的同步异步和执行机制,是写出高质量前端代码的基础。希望这篇文章能帮你理清这些核心概念!


相关推荐
触底反弹1 小时前
深入理解 JavaScript 同步与异步:从 Event Loop 到 async/await
javascript
浮生望1 小时前
JavaScript 异步编程核心:从同步阻塞到 Promise 事件循环
javascript·promise
假如让我当三天老蒯2 小时前
暂时性死区是否和闭包是相背的呢(自学用)
前端·javascript
渣波2 小时前
前端开发主页面小技巧
前端·javascript
小林ixn2 小时前
前端必知:JS同步异步与Promise,终于有人讲明白了!
javascript·面试
bonechips2 小时前
JS:同步与异步,从单线程到 Promise 的编程之路
前端·javascript
先吃饱再说2 小时前
为什么 `setTimeout` 会“插队”?JS 事件循环与 Promise 通关笔记
前端·javascript·promise
Web打印2 小时前
打印PDF面单顺序会乱 使用HttpPrinter连接打印机打印PDF快递面单,顺序会乱,请问有没有碰到过这样的问题呢?是怎么解决的
javascript
Web打印2 小时前
Httpprinter 2 、3 升级到 Httpprinter4、5的 注意事项
javascript