1. 先对 Promise 有一个初步印象
一句更容易使用的定义是:
Promise 是一个表示某项操作未来结果的对象。
这个操作将来可能成功,也可能失败。它代表了某个可能现在、将来或永远不可知的值。Promise有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败),并且只能从pending状态转变为fulfilled状态或rejected状态。可以看作当你创建一个promise的时候他默认是处于pending状态的,通过执行代码进行状态转变。 创建一个promise对象的基础代码如下:
js
const promise = new Promise((resolve, reject) => {
// 执行异步操作
if (/* 异步操作成功 */) {
resolve(value); // 将Promise的状态从pending变为fulfilled,同时传递一个值给后续的then方法
} else {
reject(error); // 将Promise的状态从pending变为rejected,同时传递一个错误给后续的catch方法
}
});
创建之后,我们可以通过.then()和.catch()方法可以处理Promise的对应状态,如下:
js
//当我们创建好了一个promise对象之后就可以调用对应的then或者catch函数
promise
.then(value => {
// 处理异步操作成功的情况
})
.catch(error => {
// 处理异步操作失败的情况
});
之后我们先看一个最小例子:
js
function getMessage() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('数据准备好了');
}, 1000);
});
}
const promise = getMessage();
console.log(promise);
promise.then(message => {
console.log(message);
});
调用 getMessage() 时,会返回一个promise对象,但一秒还没有过去,所以它无法立刻返回字符串 '数据准备好了'。它先返回一个 Promise:
text
现在:Promise { pending }
一秒后:Promise { fulfilled,结果为 '数据准备好了' }
所以这里在console.log(promise)这里,会先输出一个类似于Promise { pending }的输出。
之后我们继续往下看代码,这里就用到了前面说的.then函数, .then() 的作用你可以看是给 Promise 登记一段后续代码:等 Promise 成功后,调用这个函数,并把成功结果传给它。
这里必须区分两个东西:
js
promise // 现在就能拿到的 Promise 对象
message // Promise 成功后产生的字符串
这里当执行then()函数的时候,会把promise成功后得到的结果当作参数传递进来,对应代码中的message变量,所以这里第二个输出应该是"数据准备好了"
2. 第一个疑问:为什么可以连续调用 .then()?
假设有两个存在依赖关系的异步操作:
js
function fetchData1() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('数据1');
}, 1000);
});
}
function fetchData2(data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`基于${data}的数据2`);
}, 1000);
});
}
fetchData1()
.then(data1 => {
console.log(data1);
return fetchData2(data1);
})
.then(data2 => {
console.log(data2);
})
.catch(error => {
console.error('发生错误:', error);
});
第一次看到这里,很容易产生一个错误印象:
fetchData1这个函数为什么拥有连续的.then()和.catch()方法?
实际上,拥有这些方法的不是函数,而是函数的返回值:
js
const promise1 = fetchData1();
promise1 是 Promise 对象,所以可以调用:
js
const promise2 = promise1.then(data1 => {
console.log(data1);
return fetchData2(data1);
});
关键规则出现了:
每次调用
.then(),都会返回一个新的 Promise。
因此还能继续调用:
js
const promise3 = promise2.then(data2 => {
console.log(data2);
});
const promise4 = promise3.catch(error => {
console.error(error);
});
所谓链式调用,只是省略了中间变量:
text
promise1
↓ .then()
promise2
↓ .then()
promise3
↓ .catch()
promise4
2.1 .then() 中的 return 决定下一步拿到什么
.then() 返回的新 Promise 会怎样完成,取决于回调函数的执行结果。
返回普通值:
js
Promise.resolve(1)
.then(value => {
return value + 1;
})
.then(value => {
console.log(value); // 2
});
返回另一个 Promise:
js
fetchData1()
.then(data1 => {
return fetchData2(data1);
})
.then(data2 => {
console.log(data2);
});
第二个 .then() 会等待 fetchData2(data1) 返回的 Promise,并接收它的成功结果。
如果没有写 return:
js
fetchData1()
.then(data1 => {
fetchData2(data1);
})
.then(data2 => {
console.log(data2); // undefined
});
回调函数默认返回 undefined,所以第二个 .then() 接收到的也是 undefined。它也不会等待那个没有被返回的 fetchData2()。
如果回调函数抛出异常,新 Promise 就会失败:
js
Promise.resolve('数据')
.then(() => {
throw new Error('处理失败');
})
.catch(error => {
console.error(error.message);
});
这就是一个 .catch() 能统一处理前面链条中错误的原因:错误会沿着 Promise 链向后传递,直到遇到错误处理函数。
3. Promise 对象到底保存了什么?
Promise 内部最重要的是两个概念:状态和结果。
它只有三种状态:
text
pending 等待中
fulfilled 已成功
rejected 已失败
状态只能改变一次:
text
pending → fulfilled
或者:
text
pending → rejected
一旦完成,就不能回到 pending,也不能从成功改成失败。
创建 Promise 时,JavaScript 会提供两个函数:
js
const promise = new Promise((resolve, reject) => {
if (操作成功) {
resolve(成功结果);
} else {
reject(失败原因);
}
});
resolve(value)表示操作成功,并把value记录为成功结果。reject(error)表示操作失败,并把error记录为失败原因。.then()登记成功后要执行的代码。.catch()登记失败后要执行的代码。
这里还要避免另一个常见误区:
Promise 不等于异步操作,Promise 只是表示和传递异步操作的结果。
在下面的例子中,真正负责"一秒后通知"的是 setTimeout();Promise 负责把这次通知转换成可以被 .then() 或 await 使用的结果。
js
const promise = new Promise(resolve => {
setTimeout(() => {
resolve('完成');
}, 1000);
});
而且 new Promise() 传入的函数会立即执行,并不是写进 Promise 的代码都会自动异步:
js
console.log('A');
new Promise(resolve => {
console.log('B');
resolve();
});
console.log('C');
输出仍然是:
text
A
B
C
Promise 是结果模型,不是"把代码送去后台"的魔法盒子。
4. async/await 并没有取代 Promise
之后我们讲到现代 JavaScript 代码中经常使用的 async/await,这是一种简化原来promise的方式,在ES8标准中引入的,底层还是使用的promise机制。
更准确的关系是:
text
Promise:异步结果和组合机制
async/await:使用 Promise 的语法
4.1 async:保证函数返回 Promise
在函数前添加 async:
js
async function getData() {
return '数据1';
}
虽然函数里返回的是字符串,但调用结果仍然是 Promise:
js
const result = getData();
console.log(result); // Promise
它近似等价于:
js
function getData() {
return Promise.resolve('数据1');
}
我这里也有个理解就是你可以看作添加async之后的函数返回值会包装成一个Promise,其中的函数中原来的返回值就是Promise中的信息。
通过抛异常来转化为reject状态,如果 async 函数抛出异常:
js
async function getData() {
throw new Error('失败');
}
就近似于返回:
js
Promise.reject(new Error('失败'));
4.2 await:等待 Promise 的完成结果
接下来是关键词await,他表示:等待右侧的一个Promise 完成,然后只取得它的成功结果。
js
async function main() {
const data1 = await fetchData1();
console.log(data1);
const data2 = await fetchData2(data1);
console.log(data2);
}
可以把其中一行拆成两个概念步骤:
js
const promise = fetchData1(); // 启动操作,得到 Promise
const data1 = await promise; // 等待 Promise,取得结果
于是原来的 Promise 链:
js
fetchData1()
.then(data1 => {
console.log(data1);
return fetchData2(data1);
})
.then(data2 => {
console.log(data2);
})
.catch(error => {
console.error(error);
});
可以写成:
js
async function main() {
try {
const data1 = await fetchData1();
console.log(data1);
const data2 = await fetchData2(data1);
console.log(data2);
} catch (error) {
console.error(error);
}
}
main();
两种写法使用的是同一套 Promise 机制。async/await 主要改变的是代码的表达方式,让有先后依赖的异步流程更接近普通顺序代码。
5. 自己写代码后,为什么得到 undefined?
理解一个语法最有效的方法之一,是自己写一个最小例子。下面这个例子看似创建了随机数:
js
async function test1() {
try {
const d = Math.random() * 10;
} catch (error) {
}
}
const promise = test1();
promise.then(d => console.log(d));
console.log(await test1());
运行后却得到:
text
undefined
undefined
原因不是 async 或 await 没有生效,而是函数没有 return d。
普通函数没有返回值时,默认返回 undefined:
js
function test1() {
const d = Math.random() * 10;
return undefined;
}
async 函数没有返回值时,则近似返回:
js
Promise.resolve(undefined);
正确写法是:
js
async function test1() {
return Math.random() * 10;
}
const promise = test1();
promise.then(d => console.log('then:', d));
console.log('await:', await promise);
这里 .then() 和 await 读取的是同一个 Promise,所以得到同一个随机数。
Math.random() 本身是同步操作,真实代码不需要为了它添加 async。这里保留 async,只是为了观察"普通返回值怎样变成 Promise 结果"。
这个实验说明:
async只负责把函数返回值包装成 Promise,不会自动把函数中的局部变量当作返回值。
另外,空的 catch 会吞掉异常,让函数在失败时悄悄返回 undefined。如果确实需要捕获错误,应当记录、处理或继续抛出:
js
async function test1() {
try {
return Math.random() * 10;
} catch (error) {
console.error(error);
throw error;
}
}
旁注:顶层
await直接在文件最外层使用
await属于 ES Module 语法。如果 Node.js 没有通过.mjs扩展名或package.json中的"type": "module"明确模块类型,可能出现模块类型警告。这个问题属于 JavaScript 模块系统,不是 Promise 本身的问题。最好还是只在async函数中使用await
6. JavaScript 不是按顺序逐行执行的吗?
是的,当前正在执行的 JavaScript 代码仍然按照顺序运行。
js
console.log('A');
console.log('B');
console.log('C');
一定输出:
text
A
B
C
但是下面的代码会输出 A、C、B:
js
console.log('A');
setTimeout(() => {
console.log('B');
}, 1000);
console.log('C');
这并不是 JavaScript 发现 setTimeout() 需要等待,于是自动跳过了它。
实际过程是:
text
1. 执行 console.log('A')
2. 调用 setTimeout(),向运行环境登记定时任务
3. setTimeout() 立即返回
4. 执行 console.log('C')
5. 一秒后,定时任务到期
6. 运行环境安排回调函数执行
7. 输出 console.log('B')
所以实际上其实setTimeout() 这一行已经执行完了。它完成的工作不是"原地等待一秒",而是"登记一个一秒后需要执行的回调"。后续在任务完成时把这个回调插入到代码的执行队列中。
因此,更准确的说法是:
JavaScript 按顺序执行当前代码;某些异步 API 启动任务后会立即返回,所以 JavaScript 可以继续执行后面的代码。
7. 系统不会自动判断一个操作是否需要等待
一段代码是否阻塞,不是由它"看起来耗时"决定的,而是由 API 的设计决定的。
以 Node.js 读取文件为例:
js
const fs = require('node:fs');
const data = fs.readFileSync('test.txt', 'utf8');
console.log(data);
readFileSync() 是同步 API。文件读取完成前,当前 JavaScript 代码会停在这一行。
异步回调版本则是:
js
fs.readFile('test.txt', 'utf8', (error, data) => {
if (error) {
console.error(error);
return;
}
console.log(data);
});
console.log('文件还在读取,当前代码继续执行');
Promise 版本是:
js
const fs = require('node:fs/promises');
async function readText() {
const promise = fs.readFile('test.txt', 'utf8');
console.log('文件还在读取,当前代码继续执行');
const data = await promise;
console.log(data);
}
readText().catch(console.error);
三个 API 可以完成相似的工作,但契约不同:
| API | 如何交付结果 | 是否阻塞当前 JavaScript |
|---|---|---|
readFileSync() |
直接返回内容 | 是 |
readFile() |
完成后调用回调 | 否 |
fs.promises.readFile() |
返回 Promise | 否 |
JavaScript 不会测量一行代码有多慢,然后决定是否把它变成异步。API 是同步还是异步,在它的实现和使用方式中已经确定了。
8. await 暂停了什么?
await 不负责让操作变成异步。右侧操作是否异步,在调用 API 时就已经决定了。
js
async function loadUser() {
console.log('开始加载');
const response = await fetch('/api/user');
console.log('加载完成');
return response;
}
loadUser();
console.log('外部代码继续执行');
可以把 loadUser() 看成被 await 分成两段:
text
第一段:立即执行到 await
↓
fetch() 启动请求并返回 Promise
↓
await 暂停 loadUser(),loadUser() 返回自己的 Promise
↓
外部代码继续执行
↓
网络请求完成
↓
loadUser() 从 await 后面恢复
所以输出顺序通常是:
text
开始加载
外部代码继续执行
加载完成
关键结论是:
await暂停的是当前async函数后面的部分,不是整个 JavaScript 程序,也不是底层操作本身。
不写 await,网络请求仍然会启动:
js
const promise = fetch('/api/user');
console.log('请求已经启动,但这里没有等待结果');
这进一步说明了二者的分工:
text
fetch():启动异步操作,返回 Promise
await:等待 Promise,恢复当前函数
9. 什么时候多个异步任务可以同时进行?
先定义一个模拟异步请求的函数:
js
function request(name, ms) {
return new Promise(resolve => {
console.log(`${name} 开始`);
setTimeout(() => {
console.log(`${name} 完成`);
resolve(name);
}, ms);
});
}
9.1 连续 await:串行执行
js
async function runSerially() {
const resultA = await request('任务A', 1000);
const resultB = await request('任务B', 1000);
return [resultA, resultB];
}
任务 B 必须等任务 A 完成后才启动:
text
任务A:|------1秒------|
任务B: |------1秒------|
总计:|-------------约2秒-------------|
如果任务 B 依赖任务 A 的结果,这就是正确写法:
js
const user = await fetchUser();
const orders = await fetchOrders(user.id);
9.2 先启动再等待:并发执行
如果两个任务互不依赖,可以先把它们都启动:
js
async function runConcurrently() {
const promiseA = request('任务A', 1000);
const promiseB = request('任务B', 1000);
const results = await Promise.all([promiseA, promiseB]);
return results;
}
两个任务几乎在同一时间进入等待:
text
任务A:|------1秒------|
任务B:|------1秒------|
总计:|------约1秒------|
也可以写成:
js
const [resultA, resultB] = await Promise.all([
request('任务A', 1000),
request('任务B', 1000)
]);
这里还有一个容易被忽略的细节:
Promise.all()本身不是启动开关。调用request()时任务就启动了,Promise.all()负责等待所有 Promise 完成并收集结果。
10. 并发不等于并行
并发表示多个任务在同一段时间里都处于进行状态:
text
请求A:等待服务器响应
请求B:等待服务器响应
请求C:等待服务器响应
并行表示多个处理单元真的在同一时刻执行计算:
text
CPU 核心1:计算任务A
CPU 核心2:计算任务B
JavaScript 主线程擅长同时管理大量"正在等待"的 I/O 任务,但两段普通 JavaScript 计算不会因为使用了 Promise 就自动在不同 CPU 核心并行执行。
下面的同步计算会持续占用主线程:
js
function heavyTask() {
const end = Date.now() + 5000;
while (Date.now() < end) {
// 持续占用 JavaScript 主线程
}
}
setTimeout(() => {
console.log('定时器完成');
}, 1000);
heavyTask();
虽然定时器设置为一秒,但回调必须等 heavyTask() 五秒后释放主线程,才有机会执行。
这说明:
异步机制适合避免在 I/O 等待期间占用主线程,但不会自动解决 CPU 密集计算造成的阻塞。
真正需要并行计算时,需要浏览器的 Web Worker、Node.js 的 Worker Threads 或多进程等机制。
11. 谁在处理等待?
JavaScript 异步编程涉及多个层次,不能全部归因于 JavaScript 语言本身。
text
你的 JavaScript 代码
↓
Promise、async、await
JavaScript 语言定义的结果和控制流机制
↓
setTimeout、fetch、文件读取
浏览器或 Node.js 提供的宿主 API
↓
事件循环、I/O 调度
浏览器或 Node.js 运行环境
↓
计时、网络、文件
操作系统和底层设施
以网络请求为例,大致过程是:
text
1. JavaScript 调用 fetch()
2. 运行环境启动底层网络请求
3. fetch() 立即返回 Promise
4. JavaScript 可以继续执行其他代码
5. 操作系统收到网络数据
6. 运行环境让对应 Promise 成功或失败
7. Promise 的后续代码进入待执行队列
8. 事件循环在主线程可以执行时恢复后续代码
异步操作完成时,操作系统不会直接闯进 JavaScript 主线程执行回调。运行环境会先记录完成事件,再由事件循环安排对应代码执行。
因此,即使很多网络请求正在并发进行,它们完成后的 JavaScript 代码通常仍然在主线程上依次执行。
12. 最后的一问:为什么不能直接 await setTimeout()?
现在我们看一个关键问题:
js
async function wrongDelay() {
console.log('开始');
await setTimeout(() => {
console.log('定时器回调');
}, 1000);
console.log('await 后面的代码');
}
这段代码不会让 await 等待一秒。输出效果是:
text
开始
await 后面的代码
定时器回调
原因在于普通 setTimeout() 返回的是定时器标识:浏览器中通常是数字,Node.js 中通常是定时器对象。它返回的不是一个会在定时器到期时完成的 Promise。
await 可以接收任意值,但只有当右侧值是 Promise 或类似 Promise 的对象时,它才能把函数恢复时间与那个异步操作的完成时间关联起来。
所以需要包装一层:
js
function delay(ms) {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, ms);
});
}
async function main() {
console.log('开始');
await delay(1000);
console.log('一秒后');
}
main();
这层 Promise 不是多余代码,它完成了一次接口转换:
text
setTimeout 的回调通知
↓
定时器到期时调用 resolve()
↓
Promise 从 pending 变为 fulfilled
↓
await 恢复当前 async 函数
各部分的职责终于清晰了:
text
setTimeout:登记定时任务
Promise:表示定时任务未来会完成
resolve:发出"已经完成"的信号
await:等待这个完成信号并恢复函数
这也是理解 Promise 的一个重要入口:
当旧式异步 API 只通过回调通知结果时,可以用 Promise 把这次未来通知转换成一个标准的结果对象。
13. 用一个统一模型重新理解异步
现在可以把全文压缩成一个统一过程:
text
1. JavaScript 按顺序执行当前代码
2. 调用异步 API,启动定时器、网络或文件任务
3. 异步 API 立即返回,通常返回 Promise 或接受回调
4. JavaScript 继续执行其他代码
5. 底层任务在运行环境或操作系统中等待和处理
6. 任务完成后,运行环境记录完成事件
7. Promise 成功或失败,相关后续代码进入队列
8. 事件循环在主线程空闲时执行后续代码
于是最初的几个疑问都可以得到统一解释:
为什么可以连续调用 .then()?
因为 Promise 拥有 .then(),而每次 .then() 又返回新的 Promise。
async/await 是否取代了 Promise?
没有。async 函数返回 Promise,await 等待 Promise,它们是 Promise 的另一种表达方式。
JavaScript 逐行执行,异步为什么还能并发?
JavaScript 逐行启动多个异步任务;等待发生在运行环境或操作系统中,主线程不需要在 I/O 等待期间停住。
await 是否让一个操作变成异步?
不会。API 本身决定操作如何执行,await 只控制当前 async 函数怎样等待结果。
为什么 await setTimeout() 不等待?
因为普通 setTimeout() 没有返回表示定时器完成的 Promise。包装 Promise 是在建立"定时器完成"和"Promise 完成"之间的联系。
14. 再读一遍 Promise 的定义
现在再回头看最初那句话:
Promise 代表了某个可能现在、将来或永远不可知的值。
它可以更精确地理解为:
Promise 是一个表示某项操作最终成功结果或失败原因的对象。它可能已经完成,也可能将来完成,也可能因为从未调用
resolve()或reject()而永远保持等待状态。
例如:
js
const promise = new Promise(() => {
// 永远不调用 resolve() 或 reject()
});
await promise;
这个 Promise 会一直处于 pending,当前 async 函数也会一直停在这里。
"未来的值"并不是被提前藏在 Promise 里,而是 Promise 提供了一个稳定对象,让现在的代码可以登记:未来结果出现时,应该怎样继续。
15. 学到这里,边界在哪里?
到这里,已经建立了日常使用 JavaScript 异步编程所需的主干模型:
- Promise 表示未来结果,不等于异步操作本身。
.then()返回新 Promise,因此可以形成链。async/await建立在 Promise 之上。- JavaScript 不会自动识别耗时操作,是否异步由 API 契约决定。
await暂停当前函数,不阻塞整个程序。- 先启动多个独立任务,再统一等待,可以形成并发。
- I/O 并发与 CPU 并行是不同问题。
- 运行环境和事件循环负责在异步操作完成后恢复代码。
还有一些值得继续深入的主题:
- Promise 后续代码为什么通常早于
setTimeout(..., 0)执行。 - 任务队列与微任务队列有什么区别。
Promise.all()、allSettled()、race()和any()分别适合什么场景。- 如何处理超时、取消、重试和未捕获的 Promise rejection。
- 如何限制并发数量,避免一次启动过多请求。
这些内容属于下一层调度细节和工程实践。暂时不展开,如果后续需要的话我再写完发出来给大家看。
感谢屏幕前的你读到这里,如果有帮助的希望可以点赞收藏,这对我将是很好的激励。 以上为笔者自己初学时期在了解常用await做什么的引申出来的异步学习,自己通过一些其他的技术博客+询问AI得到了一个不错的理解,之后自己梳理了一下我的学习路径,写了个行文大纲,然后让AI填充了大部分的文章内容。这部分写在最后面也有一点我的小私心,有的人可能在一开始看到是AI生成的文章可能就不会看了,但亲爱的同学如果看到了最后面的我的这段说明即使是AI大部分生成的文章,也是可以质量不错对人有用的,重点在于你是否教会AI怎么写,写的有条理,让人能够理解。Ty。