文章

详细讲讲 Promise

通过 fetch 拿到页面数据、并渲染到页面上显示

✨ 概念扫盲

比较杂乱喵,这个 “详细讲讲” 更像是笔记大杂烩了。。

1
2
    > 👉 **`fetch()` 的响应(`Response` 对象)底层确实使用了 Stream(流)机制**,这和浏览器处理**大文件传输、视频播放、断点续传、实时数据流**等是同一套底层思想。
    >  - [ ]  fetch 返回的 Promise 对象还分响应头、响应体?还不能一次性拿到,得分开拿? - [ ]  fetch 函数都能填哪些参数?

❄️【预】补充知识

回调队列

Promise 对象含有两个回调队列,分别管理 .then() 回调成功、回调失败时的。。?

💡“回调队列”不是只属于“微任务队列”

而是一个统称,指的是那些存放“等待执行的回调函数”的队列。

浏览器确实会维护多个队列,每种队列对应不同类型的任务(定时器、网络请求、UI 渲染等)。

  • 它们总体可以分为两大类:
    • 宏任务队列(Macrotask Queue)
    • 微任务队列(Microtask Queue)
  • 换句话说:
    • 宏任务队列也是一种“回调队列”
    • 微任务队列同样也是一种“回调队列”

      它们都是“异步任务回调函数的存储地”,只是优先级和触发时机不同。

🗻 对底层感兴趣的话


一、函数对象

因为 Promise 是一个在异步函数之间传递的对象。就会想要知道 ”Promise 对象本身长什么样呢?“

Promise 结构

在 JavaScript 里,Promise 是一个有状态的对象,用来表示一个「异步操作的最终结果」。

1
2
3
4
5
6
7
{
  state: 'pending' | 'fulfilled' | 'rejected',
  value: any, // 成功时保存的结果,或失败时的错误
  // 接下来是两个回调队列
  onFulfilledCallbacks: [], // then成功回调的队列
  onRejectedCallbacks: []   // then失败回调的队列
}

举例:

  • 这里能看出,这个 Promise Error 对象包含了状态、为何错误的信息。

1

Promise 在异步函数中的职责

看到 Promise 仅由 3 部分组成,就会怀疑 “Promise 的结构真的只有这么点?”——换句话说:这种 “异步传递链” 的底层实现不应该更复杂吗?Promise 作为异步链重要的中间人,结构这么简单能行吗?

其实 Promise 并不是“魔法”——

它只需要这三类信息,就能完整地支撑“异步结果的通知机制”:

信息 为什么必要
状态(state) 用来记录任务进行到哪一步(pending / fulfilled / rejected)
结果(value/reason) 状态一旦确定,后续 .then().catch() 都需要这个结果
回调队列(handlers) 状态还没确定时,先把“要执行的函数”存起来;一旦状态确定,再把它们依次执行

Promise 的职责其实就是 「等异步结果出来后,通知所有等待它的人」

它不是执行异步任务的“引擎”,而是“异步结果的中介容器”。

异步任务(比如 setTimeout、fetch)本身是由浏览器或 Node 的 底层调度系统(事件循环、任务队列)执行的,Promise 只负责在结果出来后:

把状态改为 fulfilled/rejected → 调度注册过的回调。

二、初识 Promise ~ 从 fetch 入手

目标:用 Promise 处理“真实异步场景”,比如网络请求、延迟执行、文件加载等。

我的构想(第一步):调用fetch API,去尝试拿到一些有趣网站的200?(为了防止踩坑,先问问 AI)

fetch 基本用法

一些优点先不用管,等用熟练了再回看会更有概念。

image

直接用!(一些 API 调用的记忆开始苏醒)

1
2
3
4
5
6
// 尝试用 fetch API 来 取到一些 API 网站 的信息
const res = fetch("https://api.atlasacademy.io");

res.then((response) => {
  console.log(response);
});

返回结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Response {
  status: 200,
  statusText: 'OK',
  headers: Headers {
    'alt-svc': 'h3=":443"; ma=2592000,h3=":443"; ma=2592000',
    'cache-control': 'no-cache',
    'content-encoding': 'gzip',
    'content-type': 'text/html; charset=utf-8',
    date: 'Tue, 07 Oct 2025 04:24:39 GMT',
    'server-timing': 'app;dur=0.24',
    vary: 'Accept-Encoding',
    via: '1.1 Caddy',
    'x-frame-options': 'Deny',
    'content-length': '501'
  },
  body: ReadableStream { locked: false, state: 'readable', supportsBYOB: true },
  bodyUsed: false,
  ok: true,
  redirected: true,
  type: 'basic',
  url: 'https://api.atlasacademy.io/docs'
}

① 打印结果(Response)体现了什么?没体现什么?

fetch() 成功完成 HTTP 请求 + 收到响应头 后,JS 会构造这样一个 Response 对象返回给你。

这意味着:

  • 你的 console.log(response) 打印的内容,是 “响应头+元信息”
  • 但此时,响应体(body)还没被读取,它只是一个流(ReadableStream)。

虽然你看到了 Response 对象,但这里的 异步本质 还没完全显现。

关键在于:response.body 是一个 ReadableStream,还没转化成实际数据。

你要继续调用异步方法来“消费”这段流,比如:

1
2
3
4
fetch('https://api.atlasacademy.io/docs')
  .then(response => response.text())   // 异步读取流
  .then(html => console.log(html))      // 这里才是真正的响应内容
  
  • response.text()response.json()response.blob() 等都是 返回 Promise 的异步方法
  • 因为底层需要等浏览器从网络流中一点点读取完整 body,这不能阻塞主线程。

实际上的 fetch 异步调用过程

获取响应通常需要经过两个阶段。

第一阶段,当服务器发送了响应头(response header),fetch 返回的 promise 就使用内建的 Response class 对象来对响应头进行解析。

……第二阶段,为了获取 response body,我们需要使用一个其他的方法调用。


② 从参数 response 看 fetch/Promise 的传递

以下列出了常用的 response 方法:它们通常用在 .then() 的结尾 return 处,用于向下一次 .then() 传递一个 Promise 对象。

Response 提供了多种基于 promise 的方法,来以不同的格式访问 body:

  • response.text() —— 读取 response,并以文本形式返回 response,
  • response.json() —— 将 response 解析为 JSON 格式,
  • response.formData() —— 以 FormData 对象(在 下一章 有解释)的形式返回 response,
  • response.blob() —— 以 Blob(具有类型的二进制数据)形式返回 response,
    • 既然是二进制文件,就能想到 图片传输。
  • response.arrayBuffer() —— 以 ArrayBuffer(低级别的二进制数据)形式返回 response,
    • 主要用于获取流媒体文件。比如:获取音频文件song.ogg,然后在线播放。
  • 另外,response.body 是 ReadableStream 对象,它允许你逐块读取 body,我们稍后会用一个例子解释它。

——[Fetch](https://zh.javascript.info/fetch) ——Fetch API 教程- 阮一峰的网络日志

比如:当我向 .then() 中添加一段 ~~return response.blob()~~ 下述代码后,控制台输出会多出这么一段:

1
2
3
4
5
6
7
8
res
  .then((response) => {
    ...
    return response.blob(); // return 一个 Promise 对象,给下一个 .then()
  })
  .then((response) => {
    console.log(response);
  });
  • 这里就说明了:Promise 对象在两层 .then() 之间传递成功了。
1
2
// 这里其实是 第二层 .then() 打印的结果
Blob { size: 1031, type: 'text/html;charset=utf-8' }

所以,fetch 中 response / Promise 沿 .then() 链传递的过程如下:

具体来说:

  1. fetch() 返回一个 **Promise**
  2. .then() 拿到 Response,你返回了 response.blob()(一个新的 **Promise**)
  3. 这个新的 Promise 会被“自动接力”给下一个 .then()
  4. 所以第二个 .then() 拿到的就是 Blob(即 Promise<Blob> resolve 的结果)

这就是 Promise 链式传递机制 的核心:

上一个 .then() 返回什么,下一个 .then() 就接到什么。

③ new Promise 构造函数

一直有一个疑问:为啥第一个 new Promise 里的回调函数就是同步的?

值得注意的是,Promise 是用来管理异步编程的,它本身不是异步的,new Promise的时候会立即把executor函数执行,只不过我们一般会在executor函数中处理一个异步操作。

先举个例子:

1
2
3
const p = new Promise((resolve, reject) => {
  resolve(42);
});
  • 一开始:p.state = 'pending'
  • 调用 resolve(42)
    • 改为 state = 'fulfilled' (就是普通的变换 Promise 状态)
    • 保存结果 value = 42
    • 通知所有在 onFulfilledCallbacks 中注册的回调依次执行(但不是立即执行——要进入微任务队列)。(但这是??)(【注】:onFulfilledCallbacks: [], 是 Promise 对象的其中一个属性,代表 then 成功回调的队列。)

④ Promise 链内部传递逻辑

……(前面是极简 Promise 手写实现)

  1. 调用then方法,将想要在Promise异步操作成功时执行的回调放入callbacks队列,其实也就是注册回调函数,可以向观察者模式方向思考;
  2. 创建Promise实例时传入的函数会被赋予一个函数类型的参数,即resolve,它接收一个参数value,代表异步操作返回的结果,当异步操作执行成功后,用户会调用resolve方法,这时候其实真正执行的操作是将callbacks队列中的回调一一执行;

——30分钟,让你彻底明白Promise原理

看不懂 100% 不要紧,至少能看出这里提到了 “将 .then() 内部的回调函数,放入 callback 队列”,也就是放入回调队列。

结合事件循环的知识,可以猜出:回调队列本质,就和异步的任务队列差不多——它们会在合适的时机,将存储的任务交给(浏览器)主线程去处理。

不如说:“回调” 本身,也是任务队列的一种属性(不限于宏 or 微任务队列的其中一者)。

回调是任务队列的内容,而不是类别。”


三、异步链本质

——关于这一点:还是要回到 JS 的事件循环 这一块。

请看:微任务队列

当你调用:

1
const response = await fetch(url);

实际上浏览器做了这些事:

  1. 主线程发出请求(立即返回 Promise 对象)
  2. 网络层(浏览器 IO 线程)在后台等待服务器响应
  3. 收到响应头后,构造 Response 对象 → Promise 变为 fulfilled
  4. 主线程进入 .then()await 后续代码
  5. 如果调用 response.text(),再次进入异步等待“读取整个 body”

👉 所以整个 fetch 流程其实是“两层异步”:

1
2
fetch() -> 等响应头
response.text()/json() -> 等响应体

四、上手 ~ 熟练地使用 Promise 函数

1️⃣ 基础方法:then()、catch()、finally()

  • .then() 回调函数的参数机制:

    .then() 中能用的参数(比如 htmlContentresponsedataresult 等)不是固定的,它的名字是你自己定义的变量名。真正决定你能拿到什么内容的,是上一个 Promise 返回的值

.finally() 就像 Promise 链中的一个 “结束标识符” —— 不管成功还是失败,它都会执行,用来收尾或清理

2️⃣ 组合方法:all()、race()、allSettled()、any()

首先,我们要理解 “同时 / 并发 执行多个异步任务” 时,底层从执行开始、到执行结束中发生的事情。

我们要理解:在使用

1
Promise.all()

这种 并发执行多个异步任务 的场景中,——从 “任务被发起”“任务全部结束” 的整个底层过程里,JavaScript 引擎、事件循环、以及异步机制之间 是如何协作的

比如:

1
2
3
4
async function runParallel() {
  const promises = [delay(1000, "A"), delay(500, "B"), delay(300, "C")];
  await Promise.all(promises);
}

A、B、C 这三个异步任务,它们不会互相等待,而是几乎在同一时间被 JavaScript 事件循环调度、并“同时”进入执行队列

但这只是 “最终实现的结果”、“表象”——首先有个问题:JS 作为一个单线程语言,是如何做到多线程并发的?

JavaScript 在浏览器和 Node.js 里都是单线程的,也就是说:

同一时刻只能执行一段代码。

那它怎么能“同时”跑多个任务呢?

本文由作者按照 CC BY 4.0 进行授权