一、Promise核心概念
什么是Promise?
Promise是ES6引入的异步编程解决方案,用于解决传统回调函数嵌套(回调地狱)的问题。
你可以把它理解为一个"承诺":
-
承诺在未来某个时间点会给出结果(成功/失败)。
-
一旦结果确定,就不会再改变。
-
可以通过链式调用替代嵌套回调。
Promise的三种状态。
Promise有且仅有三种状态,状态一旦改变就不可逆:
-
pending(进行中):初始状态。
-
fulfilled(已成功):异步操作完成。
-
rejected(已失败):异步操作出错。
基本语法。
// 创建Promise实例。
const promise = new Promise((resolve, reject) => {
// 异步操作(比如接口请求、文件读取)。
setTimeout(() => {
const success = true; // 模拟操作结果。
if (success) {
resolve("操作成功"); // 成功时调用,状态变为fulfilled。
} else {
reject("操作失败"); // 失败时调用,状态变为rejected。
}
}, 1000);
});
// 消费Promise。
promise
.then((result) => {
// 成功回调:接收resolve的参数。
console.log("成功:", result);
})
.catch((error) => {
// 失败回调:接收reject的参数。
console.log("失败:", error);
})
.finally(() => {
// 无论成功/失败都会执行(ES2018新增)。
console.log("操作结束");
});
// 多个then、catch时,链式调用该如何写?详情请参考本章第72.5节。
代码解释:
-
new Promise()接收一个执行器函数,参数是resolve(成功回调)和reject(失败回调)。
-
then()处理成功状态,catch()处理失败状态,finally()处理收尾操作。
-
执行器函数会立即执行,异步操作的结果通过resolve/reject传递。
二、Promise核心方法
链式调用(解决回调地狱)。
// 传统回调地狱(嵌套)。
ajax("url1", (res1) => {
ajax("url2?data=" + res1, (res2) => {
ajax("url3?data=" + res2, (res3) => {
console.log(res3);
});
});
});
// Promise链式调用(扁平化)。
function request(url) {
return new Promise((resolve) => {
ajax(url, (res) => resolve(res));
});
}
request("url1")
.then((res1) => request("url2?data=" + res1))
.then((res2) => request("url3?data=" + res2))
.then((res3) => console.log(res3))
.catch((err) => console.log(err));
// 每个then不返回值或返回值是非Promise值时怎么办?详情请参考本章第72.6节。
常用静态方法。
| Promise.resolve(value) | 快速创建一个已成功的Promise。 | 同步值转异步、统一返回值类型。 |
| Promise.reject(reason) | 快速创建一个已失败的Promise。 | 主动抛出异常。 |
| Promise.all([p1,p2,p3]) | 所有Promise都成功才成功,一个失败就失败。 | 并行请求多个接口,需全部返回后处理。 |
| Promise.race([p1,p2,p3]) | 第一个完成(无论成功 / 失败)的Promise决定结果。 | 超时控制(比如请求5秒未响应则中断)。 |
| Promise.allSettled([p1,p2,p3]) | 所有Promise都完成(无论成败),返回所有结果。 | 并行请求,需知道每个请求的结果(成功 / 失败)。 |
| Promise.any([p1,p2,p3]) | 第一个成功的Promise决定结果,全部失败才失败。 | 多源请求(比如多个CDN地址,取第一个可用的)。 |
示例:Promise.all实战。
// 并行请求3个接口。
const p1 = fetch("/api/user");
const p2 = fetch("/api/order");
const p3 = fetch("/api/goods");
Promise.all([p1, p2, p3])
.then((responses) => {
// 所有请求成功,处理结果。
return Promise.all(responses.map(res => res.json()));
})
.then(([user, order, goods]) => {
console.log("用户数据:", user);
console.log("订单数据:", order);
console.log("商品数据:", goods);
})
.catch((err) => {
// 任意一个请求失败,触发catch
console.log("请求失败:", err);
});
示例:Promise.race 实现超时控制。
// 封装超时函数。
function timeout(delay) {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error("请求超时")), delay);
});
}
// 请求接口,5秒超时。
function fetchWithTimeout(url) {
return Promise.race([
fetch(url),
timeout(5000)
]);
}
// 使用。
fetchWithTimeout("/api/data")
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.log(err)); // 5秒未响应则输出"请求超时"。
三、Promise实战场景
封装异步API(比如定时器/文件读取)。
// 封装setTimeout为Promise。
function delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
// 使用。
delay(2000).then(() => {
console.log("2秒后执行");
});
// 封装Node.js文件读取。
const fs = require("fs");
function readFile(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, "utf8", (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
// 使用。
readFile("./test.txt")
.then(data => console.log(data))
.catch(err => console.log(err));
/*
根据宏任务/微任务机制,
使用Nodejs文件读取时,异步的是Promise.then而不是Promise中的函数,
文件读取时依旧会阻塞主进程吗?
详情请参考本章第72.9节。
*/
结合async / await(Promise语法糖)。
async / await是ES2017引入的,基于Promise实现,让异步代码看起来像同步代码:
// 基础用法。
async function getData() {
try {
// 等待Promise完成。
const res = await fetch("/api/data");
const data = await res.json();
console.log(data);
} catch (err) {
// 捕获所有异常(等价于catch)。
console.log("请求失败:", err);
}
}
// 并行执行(优化性能)。
async function getMultiData() {
// 先发起所有请求(并行)。
const p1 = fetch("/api/user");
const p2 = fetch("/api/order");
// 再等待所有结果。
const userRes = await p1;
const orderRes = await p2;
const user = await userRes.json();
const order = await orderRes.json();
return { user, order };
}
// async / await具体用法请参考本章第72.7节。
实际项目中的常见用法。
// 场景1:表单提交(先验证,再请求,最后提示)。
async function submitForm(formData) {
try {
// 1. 前端验证。
if (!formData.username) throw new Error("请输入用户名");
// 2. 提交请求。
const res = await fetch("/api/submit", {
method: "POST",
body: JSON.stringify(formData)
});
const data = await res.json();
// 3. 处理结果。
if (data.code === 200) {
alert("提交成功");
window.location.href = "/success";
} else {
alert(data.message);
}
} catch (err) {
alert("提交失败:" + err.message);
}
}
// 场景2:数据缓存(避免重复请求)。
const cache = new Map();
async function getCachedData(key) {
// 有缓存则直接返回。
if (cache.has(key)) return cache.get(key);
// 无缓存则请求并缓存。
const res = await fetch(`/api/data?key=${key}`);
const data = await res.json();
cache.set(key, data);
return data;
}
四、常见坑点与避坑技巧
忘记catch异常:未捕获的Promise异常会导致程序崩溃,务必在链式调用末尾加catch,或用try/catch包裹await。
错误的链式调用:then中未返回新Promise会导致后续then立即执行,而非等待异步操作。
// 错误写法。
request("url1").then(() => {
request("url2"); // 未返回,后续then会立即执行。
}).then(() => {
console.log("url2 请求完成"); // 实际url2还在请求中。
});
// 正确写法。
request("url1").then(() => {
return request("url2"); // 返回Promise,等待完成。
}).then(() => {
console.log("url2 请求完成");
});
Promise.all快速失败:只要有一个Promise失败,整个all就会失败,若需全部结果,改用allSettled。
五、Promise链式调用异常捕获
-
问题:
在一串全是then的Promise链式调用中,该如何正确编写catch来捕获整个链路上的异常?
-
解决:
在链式调用末尾加一个catch。
这是最推荐的写法 —— 无论链式调用中有多少个then,只需要在整个链条的最后添加一个catch,就能捕获链路上任意位置抛出的异常(包括任意then里的同步错误、任意Promise的reject)。
-
原因:
Promise链式调用的本质是“异常冒泡”:链路上任意一个环节抛出异常(reject或同步错误),都会跳过后续所有then,直接走到最近的catch。因此,末尾的一个catch就能兜底整个链路的异常。
-
进阶场景(局部捕获 + 全局兜底):
如果你想对某个then的异常做“局部处理”,处理完后继续执行后续链路,同时保留全局兜底catch,可以在对应then后紧跟一个局部catch(处理后需返回新的Promise或值,让链路继续)。
示例代码(局部+全局):
request1()
.then((res1) => {
console.log("第一步结果:", res1);
return request2(res1);
})
// 局部catch:只捕获request2的异常。
.catch((err) => {
console.error("request2 异常(局部处理):", err);
return "request2 异常后的默认值"; // 返回值,让链路继续。
})
.then((res2) => {
console.log("第二步结果(含异常兜底):", res2);
return request3(res2);
})
.then((finalRes) => {
console.log("最终结果:", finalRes);
})
// 全局兜底:捕获其他环节的异常(比如request3、后续then)。
.catch((err) => {
console.error("全局异常兜底:", err);
}); -
常见错误写法(避坑):
-
错误 1:每个then后都加catch(冗余且易断链)。
// 不推荐!冗余且可能导致链路中断。
request1()
.then((res1) => request2(res1))
.catch((err) => console.log("err1:", err)) // 捕获后未返回值,后续then拿到undefined。
.then((res2) => request3(res2)) // res2是undefined,可能导致request3出错。
.catch((err) => console.log("err2:", err)); -
错误 2:把catch写在中间(无法捕获后续异常)。
// 错误!catch写在中间,无法捕获后续then的异常。
request1()
.then((res1) => request2(res1))
.catch((err) => console.log("中间 catch:", err))
.then((res2) => request3(res2)) // 这里的异常无法被前面的catch捕获。
.then((finalRes) => console.log(finalRes));
-
-
建议:
结合async / await的写法(更易读)。
如果你的环境支持ES2017+,用async/await + try/catch替代纯then链式调用,代码会更直观(本质还是Promise,只是语法糖)。
async function fetchData() {
try {
const res1 = await request1();
console.log("第一步结果:", res1);const res2 = await request2(res1);
console.log("第二步结果:", res2);const finalRes = await request3(res2);
console.log("最终结果:", finalRes);
} catch (err) {
// 一个try/catch捕获所有await环节的异常。
console.error("捕获到异常:", err);
}
}fetchData();
六、Promise链式调用返回值
如果在Promise链式调用的then里不返回Promise,而是返回普通值,后续还能不能调用then —— 答案是:完全可以!
无论then里返回的是普通值(数字、字符串、对象等)还是undefined,Promise都会自动把这个值包装成一个已成功的Promise,因此后续依然可以正常调用then。
举例说明:
-
示例1:返回普通值(字符串 / 数字)。
// 初始Promise。
Promise.resolve(10)
// 第一个then:返回普通数字。
.then((num) => {
console.log("第一步:", num); // 输出:第一步:10。
return num * 2; // 返回普通值20(非Promise)。
})
// 第二个then:依然可以调用,接收上一步的返回值。
.then((num) => {
console.log("第二步:", num); // 输出:第二步:20。
return `最终结果:${num}`; // 返回普通字符串。
})
// 第三个then:继续调用。
.then((str) => {
console.log("第三步:", str); // 输出:第三步:最终结果:20。
})
.catch((err) => console.error("异常:", err)); -
示例2:返回对象/不返回值(默认返回undefined)。
Promise.resolve("初始值")
.then((val) => {
// 返回普通对象。
return { name: "测试", value: val };
})
.then((obj) => {
console.log("对象值:", obj); // 输出:对象值:{ name: '测试', value: '初始值' }。
// 不写return,默认返回undefined。
})
.then((emptyVal) => {
console.log("空值:", emptyVal); // 输出:空值:undefined。
}); -
示例3:混合返回(普通值 + Promise)。
Promise.resolve(1)
.then((num) => {
return num + 1; // 普通值2。
})
.then((num) => {
// 返回Promise。
return new Promise((resolve) => setTimeout(() => resolve(num * 10), 500));
})
.then((num) => {
console.log("混合结果:", num); // 输出:混合结果:20。
return "结束"; // 普通值。
})
.then((str) => {
console.log(str); // 输出:结束。
});
背后的原理(Promise的“同化”机制):
Promise设计了一套返回值同化规则,确保then的返回值始终是Promise,因此链式调用能一直延续。
如果then的回调返回普通值(非Promise、非抛出异常)→ 自动包装为Promise.resolve(返回值)。
如果then的回调返回Promise → 直接使用这个Promise,等待其状态变更。
所以说,不返回Promise时,then执行完后会立即执行下一个then。
如果then的回调抛出异常(throw new Error())→ 自动包装为Promise.reject(异常)。
简单来说:then的返回值永远会被“转换成”一个Promise,这就是为什么不管返回什么,后续都能调用then/catch。
异常场景(返回普通值但抛出错误):
即使返回普通值,只要then里抛出异常,依然会被后续的catch捕获。
Promise.resolve(5)
.then((num) => {
if (num === 5) {
throw new Error("手动抛出异常"); // 抛出错误。
}
return num + 1;
})
.then((num) => {
console.log("这一步不会执行"); // 异常会跳过后续then。
})
.catch((err) => {
console.error("捕获异常:", err.message); // 输出:捕获异常:手动抛出异常。
});
和返回Promise的对比(清晰版):
| return 100。 | return Promise.resolve(100)。 | 100。 |
| return {a: 1}。 | return Promise.resolve({a: 1})。 | {a: 1}。 |
| return undefined(不写return)。 | return Promise.resolve(undefined)。 | undefined。 |
| return Promise.resolve(200)。 | 直接返回该Promise。 | 200。 |
| return Promise.reject("失败")。 | 直接返回该Promise。 | 无(触发catch)。 |
| throw new Error("错误")。 | return Promise.reject(Error)。 | 无(触发catch)。 |
七、async/await用法
async/await核心本质:
async/await是ES2017(ES8)引入的异步编程语法糖,完全基于Promise实现,目的是让异步代码的写法更接近同步代码,解决Promise链式调用(then/catch)的可读性问题。
核心结论:
-
async函数的返回值永远是一个Promise(即使你返回普通值,也会被自动包装成Promise.resolve(值))。
-
await只能在async函数内部使用,作用是“暂停”函数执行,等待右侧Promise完成(成功/失败)后再继续。
基础语法(入门):
async关键字(修饰函数)。
// 1. 声明 async 函数。
async function fn1() {
return 100; // 等价于return Promise.resolve(100)
}
// 2. 箭头函数形式。
const fn2 = async () => {
return "hello"; // 等价于return Promise.resolve("hello")。
};
// 3. 验证返回值是Promise。
fn1().then(res => console.log(res)); // 输出:100。
fn2().then(res => console.log(res)); // 输出:hello。
// 4. 若async函数抛出异常,返回失败的Promise。
async function fn3() {
throw new Error("手动抛出错误"); // 等价于return Promise.reject(Error)。
}
fn3().catch(err => console.log(err.message)); // 输出:手动抛出错误。
await关键字(等待Promise完成)。
await后可以跟3类值:
-
Promise对象:等待其状态变为fulfilled,并返回resolve的值。
-
普通值(数字、字符串等):直接返回该值(无等待)。
-
异步函数(本质还是返回Promise):等待其执行完成并返回结果。
// 模拟异步函数(返回Promise)。
function delay(ms) {
return new Promise(resolve => setTimeout(() => resolve(ms), ms));
}
// async/await基本使用。
async function testAwait() {
console.log("开始执行");
/*
等待Promise完成,拿到结果。
1、执行delay(1000):立即返回一个“pending状态”的Promise,同时启动1秒的定时器。
2、遇到await:暂停testAwait函数的执行(但不会阻塞外部主线程),主线程继续执行后续同步代码(比如下述示例中的console.log("主线程同步代码:…"))。
3、1秒后:delay(1000)的Promise状态变为fulfilled,await拿到resolve的值(1000),赋值给res1。
4、恢复执行:testAwait函数从暂停处继续,执行console.log("第一个延迟完成:", 1000)。
*/
const res1 = await delay(1000);
console.log("第一个延迟完成:", res1); // 1秒后输出:1000。
// 等待普通值(直接返回)。
const res2 = await 200;
console.log("普通值:", res2); // 立即输出:200。
// 等待另一个async函数。
const res3 = await testAwait2();
console.log("async 函数结果:", res3); // 输出:testAwait2 结果。
return "最终结果"; // 等价于Promise.resolve("最终结果")。
}
async function testAwait2() {
return "testAwait2 结果";
}
/*
testAwait()是async函数,调用后返回Promise,因此需要用then接收其返回值。
如果testAwait内部抛出异常,需用.catch()捕获(或在函数内部用try/catch)。
*/
testAwait().then(res => console.log(res)); // 最终输出:最终结果。
// 同步代码:验证await不阻塞外部主线程。
console.log("主线程同步代码:testAwait调用后立即执行");
/*
输出顺序:
开始执行
主线程同步代码:testAwait调用后立即执行
第一个延迟完成: 1000
普通值: 200
async 函数结果: testAwait2 结果
最终结果
*/
// 问题:上述delay()函数中resolve(ms)不应该是调用的是.then(() => {})中的函数吗?为什么会是直接返回ms值给res1?
/*
先明确核心结论:
await是Promise的then回调的语法糖:"await promise"等价于「把后续代码塞进promise.then()的回调里,并自动提取resolve的参数作为返回值」。
简单说:你不用写then,是因为await帮你“偷偷”完成了then的工作,直接把resolve(ms)的参数(ms)返回给你。
对比两种写法(原生Promise vs async/await):
1. 原生Promise(手动写then)。
这是你认知中“resolve调用then回调”的写法:
function delay(ms) {
return new Promise(resolve => setTimeout(() => resolve(ms), ms));
}
// 原生Promise:必须写then才能拿到resolve的值。
delay(1000).then(res1 => {
console.log("第一个延迟完成:", res1); // res1就是resolve(ms)的ms。
});
2. async/await(语法糖,省略then)。
async function testAwait() {
console.log("开始执行");
// await帮你做了then的工作,直接拿到resolve的值。
const res1 = await delay(1000);
console.log("第一个延迟完成:", res1);
}
底层等价转换(await → then)。
await本质上是把代码“拆解”成了Promise的then链式调用,上面的testAwait等价于:
function testAwait() {
console.log("开始执行");
// await被拆解为then回调。
return delay(1000).then(res1 => {
console.log("第一个延迟完成:", res1);
// 如果有后续代码,会继续链式调用then。
});
}
详细拆解"await delay(1000)"的执行过程:
1. 执行delay(1000),返回一个pending状态的Promise,同时启动1秒定时器。
2. 遇到await,JS引擎会:
2.1. 暂停当前testAwait函数的执行。
2.2. 给这个Promise绑定一个「隐式的then回调」,回调的逻辑是:“把resolve的参数赋值给res1,然后恢复testAwait函数执行”。
3. 1秒后,定时器触发,执行resolve(1000)。
3.1. Promise状态变为fulfilled。
3.2. 触发步骤2中绑定的「隐式then回调」。
3.3. 回调把1000作为返回值,赋值给res1。
4. testAwait函数恢复执行,继续执行console.log("第一个延迟完成:", res1)。
补充:reject对应的逻辑(异常捕获)。
同理,await也帮你处理了catch的逻辑:
// 模拟失败的 Promise
function delayError(ms) {
return new Promise((_, reject) => setTimeout(() => reject("超时了"), ms));
}
async function test() {
try {
// await会捕获reject的值,抛出异常。
const res = await delayError(1000);
} catch (err) {
// 等价于delayError(1000).catch(err => { … })。
console.log("捕获异常:", err); // 输出:捕获异常:超时了。
}
}
总结:
resolve(ms)本身确实是触发then回调,但await帮你隐式绑定了这个then回调,并自动把回调的参数(ms)作为返回值。
"await promise"的本质:等待promise状态变为fulfilled,然后提取resolve的参数作为自己的返回值(省去了手动写then的步骤)。
记忆要点:await是Promise的then回调的语法糖,核心功能就是「自动提取resolve的值 + 暂停函数执行」。
这也是async/await比纯Promise链式调用更简洁的原因 —— 它帮你封装了then的底层逻辑,让代码看起来像同步代码。
异常处理:
await等待的Promise若变为rejected,会直接抛出异常,需用try/catch捕获(替代Promise的catch)。
基础捕获:单个await异常。
async function testError() {
try {
// 等待一个失败的Promise。
const res = await Promise.reject("请求失败");
console.log(res); // 这行不会执行。
} catch (err) {
console.error("捕获异常:", err); // 输出:捕获异常:请求失败。
}
}
testError();
批量捕获:多个await异常。
async function testMultiError() {
try {
const res1 = await delay(1000);
const res2 = await Promise.reject("第二个请求失败"); // 抛出异常。
const res3 = await delay(2000); // 这行不会执行。
} catch (err) {
console.error("捕获异常:", err); // 输出:捕获异常:第二个请求失败。
} finally {
console.log("无论成败都会执行"); // 输出:无论成败都会执行。
}
}
testMultiError();
局部捕获:特定await异常。
若想让某个 await 异常不影响后续代码,可给单个 await 加独立的 try/catch:
async function testPartialError() {
// 第一个await独立捕获异常。
let res1;
try {
res1 = await Promise.reject("请求1失败");
} catch (err) {
console.error("请求1异常:", err);
res1 = "请求1默认值"; // 兜底默认值。
}
// 异常不影响后续执行。
const res2 = await delay(500);
console.log("res1:", res1); // 输出:请求1默认值。
console.log("res2:", res2); // 输出:500。
}
testPartialError();
实战场景:
并行执行(优化性能)。
注意:默认情况下await是串行执行(等上一个完成再执行下一个),若多个异步操作无依赖,需手动改为并行,避免性能浪费。
// 反面示例:串行执行(总耗时1000+2000=3000ms)。
async function serialExec() {
console.time("serial");
const res1 = await delay(1000);
const res2 = await delay(2000);
console.timeEnd("serial"); // 输出:serial: 3000+ms。
}
// 正确示例:并行执行(总耗时2000ms,取最长的那个)。
async function parallelExec() {
console.time("parallel");
// 先发起所有异步请求(并行)。
const p1 = delay(1000);
const p2 = delay(2000);
// 再等待所有结果。
const res1 = await p1;
const res2 = await p2;
console.timeEnd("parallel"); // 输出:parallel: 2000+ms。
}
serialExec();
parallelExec();
/*
问题:
Promise不是要调用才会执行吗?"const p1/p2 = delay(1000/2000);"只是返回了一个Promise,怎么就成了并行执行呢?
解答:
Promise的执行器函数(new Promise里的代码)在创建Promise时就会立即同步执行,而非等到await/then调用时才执行。
核心原理:Promise“创建即执行”。
Promise有一个极其关键的特性:当你调用返回Promise的函数(比如delay(1000))时,
函数内部的new Promise(executor)会立即执行「执行器函数(executor)」。
也就是说,异步操作(比如setTimeout)在delay(1000)调用的瞬间就已经启动,而非等到await时才启动。
后续的"await p1/await p2"只是“等待它们完成(即订阅结果)”,而非“启动它们”。
总结:
核心原因:Promise执行器函数“创建即执行” —— 调用delay(1000)时,定时器立即启动,"const p1 = delay(1000)"只是接收返回的Promise,而非“延迟执行”。
并行关键:先调用所有返回Promise的函数(启动所有异步操作),再用await等待结果,此时异步操作已经并行运行。
串行 vs 并行:
串行:await紧跟函数调用(先等完成,再启动下一个)。
并行:先调用所有函数(启动所有操作),再await结果。
简单记:Promise的异步操作在“创建时启动”,await只负责“等结果”,不负责“启动操作” —— 这就是为什么"const p1 = delay(1000)"能触发并行执行。
*/
结合Promise.all批量并行。
async function testPromiseAll() {
try {
// 并行发起多个请求。
const promises = [delay(500), delay(1000), delay(1500)];
const results = await Promise.all(promises);
console.log("批量结果:", results); // 输出:[500, 1000, 1500]。
} catch (err) {
console.error("任意一个请求失败:", err);
}
}
testPromiseAll();
超时控制(结合Promise.race)。
// 封装超时函数。
function timeout(delay) {
return new Promise((_, reject) => setTimeout(() => reject(new Error("超时")), delay));
}
// 带超时的请求。
async function fetchWithTimeout(url) {
try {
// race:谁先完成就取谁的结果。
const res = await Promise.race([fetch(url), timeout(5000)]);
return res.json();
} catch (err) {
console.error("请求失败:", err.message);
}
}
异步遍历(for…of + await)。
不要在forEach中用await(forEach不等待异步操作,会直接遍历完),改用for…of。
// 错误示例:forEach不等待await。
async function wrongForEach() {
const arr = [1000, 2000, 500];
arr.forEach(async (ms) => {
const res = await delay(ms);
console.log("forEach 结果:", res); // 输出顺序混乱(500 → 1000 → 2000)。
});
}
// 正确示例:for…of等待await。
async function correctForOf() {
const arr = [1000, 2000, 500];
for (const ms of arr) {
const res = await delay(ms);
console.log("for…of 结果:", res); // 输出顺序:1000 → 2000 → 500。
}
}
wrongForEach();
correctForOf();
async/await vs Promise:
| 代码风格 | 同步式写法,可读性高。 | 链式调用,嵌套少但仍有回调感。 |
| 异常处理 | try/catch(同步式)。 | catch方法(回调式)。 |
| 调试体验 | 可直接断点调试(和同步代码一致)。 | 断点需跳转到then回调,体验差。 |
| 并行执行 | 需手动处理(先创建Promise再await)。 | 直接用Promise.all / Promise.race。 |
| 兼容性 | ES8(Node.js 7.6+ / 浏览器ES8+)。 | ES6(兼容性更广)。 |
八、总结
Promise:
核心本质:Promise是异步操作的"容器",通过状态管理解决回调地狱,状态一旦改变不可逆。
核心用法:then/catch/finally处理结果,all/race/allSettled处理多Promise场景,async/await简化语法。
实战关键:封装异步API统一返回Promise,结合async/await编写易读的异步代码,注意异常捕获和性能优化(如并行请求)。
async/await:
核心本质:async/await是Promise的语法糖,async函数返回Promise,await暂停函数执行等待Promise完成。
异常处理:必须用try/catch捕获await抛出的异常(替代Promise的catch)。
性能优化:无依赖的异步操作需手动并行(先创建Promise再await / 结合Promise.all),避免串行浪费时间。
避坑要点:不要在forEach中用await,改用for…of。await只能在async函数内使用。
九、问题思考
// 封装Node.js文件读取。
const fs = require("fs");
function readFile(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, "utf8", (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
// 使用。
readFile("./test.txt")
.then(data => console.log(data))
.catch(err => console.log(err));
/*
根据宏任务/微任务机制,
使用Nodejs文件读取时,异步的是Promise.then而不是Promise中的函数,
文件读取时依旧会阻塞主进程吗?
*/
结合宏任务/微任务机制,Node.js中fs.readFile封装成Promise后,是否会阻塞主进程?
核心结论:fs.readFile本身是异步的,不会阻塞Node.js主进程;Promise执行器函数里的异步IO操作和then的微任务特性,都是非阻塞的。
下述从「阻塞本质」「Node.js异步IO原理」「宏/微任务和执行时机」三个维度来进行讲解。
明确核心概念:Node.js中“阻塞”的本质。
Node.js是单线程事件循环模型,但它的“单线程”仅指「JavaScript执行线程」(主进程),而文件IO、网络请求等耗时操作,会被Node.js交给「libuv底层线程池」处理。这是Node.js异步IO的核心。
阻塞操作:占用JavaScript主线程,导致事件循环卡住(比如fs.readFileSync同步读取文件)。
非阻塞操作:JavaScript主线程仅发起请求,底层线程池处理耗时操作,完成后通过回调通知主线程(比如fs.readFile异步读取)。
关键结论:fs.readFile不会阻塞主进程。
function readFile(path) {
return new Promise((resolve, reject) => {
// Promise执行器函数(同步执行)。
fs.readFile(path, "utf8", (err, data) => { // 异步IO。
if (err) reject(err);
else resolve(data);
});
});
}
执行器函数是“同步调用异步方法”,而非“同步阻塞”。
-
Promise执行器函数((resolve, reject) => {…})会立即同步执行,但执行的是fs.readFile这个「异步方法的调用」—— 这个调用本身只做一件事:把“文件读取任务”交给libuv线程池,然后主线程立即退出执行器函数,继续处理后续代码。
-
文件读取的耗时操作(磁盘IO)是在libuv线程池中完成的,和JavaScript主线程无关,因此不会阻塞主进程。
对比同步读取(真正的阻塞)。
-
如果用fs.readFileSync(同步读取),才会真正阻塞主线程。
// 同步读取:阻塞主线程,事件循环卡住。
function readFileSync(path) {
return new Promise((resolve) => {
// 执行器函数里的同步阻塞操作!
const data = fs.readFileSync(path, "utf8");
resolve(data);
});
}此时,fs.readFileSync会占用主线程直到文件读取完成,期间Node.js无法处理任何其他请求(比如新的HTTP请求、定时器)。
宏任务/微任务视角:异步的不是then,而是IO本身。
上述提到的问题:“异步的是 Promise.then 而不是 Promise 中的函数”。这个理解需要修正:
完整执行流程(结合事件循环):
#mermaid-svg-enVGEbkA4lcCWUij{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-enVGEbkA4lcCWUij .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-enVGEbkA4lcCWUij .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-enVGEbkA4lcCWUij .error-icon{fill:#552222;}#mermaid-svg-enVGEbkA4lcCWUij .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-enVGEbkA4lcCWUij .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-enVGEbkA4lcCWUij .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-enVGEbkA4lcCWUij .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-enVGEbkA4lcCWUij .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-enVGEbkA4lcCWUij .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-enVGEbkA4lcCWUij .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-enVGEbkA4lcCWUij .marker{fill:#333333;stroke:#333333;}#mermaid-svg-enVGEbkA4lcCWUij .marker.cross{stroke:#333333;}#mermaid-svg-enVGEbkA4lcCWUij svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-enVGEbkA4lcCWUij p{margin:0;}#mermaid-svg-enVGEbkA4lcCWUij .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-enVGEbkA4lcCWUij .cluster-label text{fill:#333;}#mermaid-svg-enVGEbkA4lcCWUij .cluster-label span{color:#333;}#mermaid-svg-enVGEbkA4lcCWUij .cluster-label span p{background-color:transparent;}#mermaid-svg-enVGEbkA4lcCWUij .label text,#mermaid-svg-enVGEbkA4lcCWUij span{fill:#333;color:#333;}#mermaid-svg-enVGEbkA4lcCWUij .node rect,#mermaid-svg-enVGEbkA4lcCWUij .node circle,#mermaid-svg-enVGEbkA4lcCWUij .node ellipse,#mermaid-svg-enVGEbkA4lcCWUij .node polygon,#mermaid-svg-enVGEbkA4lcCWUij .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-enVGEbkA4lcCWUij .rough-node .label text,#mermaid-svg-enVGEbkA4lcCWUij .node .label text,#mermaid-svg-enVGEbkA4lcCWUij .image-shape .label,#mermaid-svg-enVGEbkA4lcCWUij .icon-shape .label{text-anchor:middle;}#mermaid-svg-enVGEbkA4lcCWUij .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-enVGEbkA4lcCWUij .rough-node .label,#mermaid-svg-enVGEbkA4lcCWUij .node .label,#mermaid-svg-enVGEbkA4lcCWUij .image-shape .label,#mermaid-svg-enVGEbkA4lcCWUij .icon-shape .label{text-align:center;}#mermaid-svg-enVGEbkA4lcCWUij .node.clickable{cursor:pointer;}#mermaid-svg-enVGEbkA4lcCWUij .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-enVGEbkA4lcCWUij .arrowheadPath{fill:#333333;}#mermaid-svg-enVGEbkA4lcCWUij .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-enVGEbkA4lcCWUij .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-enVGEbkA4lcCWUij .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-enVGEbkA4lcCWUij .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-enVGEbkA4lcCWUij .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-enVGEbkA4lcCWUij .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-enVGEbkA4lcCWUij .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-enVGEbkA4lcCWUij .cluster text{fill:#333;}#mermaid-svg-enVGEbkA4lcCWUij .cluster span{color:#333;}#mermaid-svg-enVGEbkA4lcCWUij div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-enVGEbkA4lcCWUij .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-enVGEbkA4lcCWUij rect.text{fill:none;stroke-width:0;}#mermaid-svg-enVGEbkA4lcCWUij .icon-shape,#mermaid-svg-enVGEbkA4lcCWUij .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-enVGEbkA4lcCWUij .icon-shape p,#mermaid-svg-enVGEbkA4lcCWUij .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-enVGEbkA4lcCWUij .icon-shape rect,#mermaid-svg-enVGEbkA4lcCWUij .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-enVGEbkA4lcCWUij .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-enVGEbkA4lcCWUij .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-enVGEbkA4lcCWUij :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
调用readFile('./test.txt')
同步执行Promise执行器函数
调用fs.readFile,交给libuv线程池处理
主线程立即返回,继续执行后续代码(非阻塞)
libuv线程池完成文件读取
将回调加入IO回调宏任务队列
事件循环到IO回调阶段
执行回调,调用resolve(data)
将then回调加入微任务队列
当前宏任务执行完
执行微任务队列中的then回调
关键细节(宏/微任务与阻塞的关系)。
-
Promise执行器函数:同步执行,但仅“发起异步IO请求”,不阻塞。
-
fs.readFile的异步性:核心是「IO操作在底层线程池执行」,和Promise无关 —— 即使不用Promise封装,直接用回调的fs.readFile也不会阻塞。
-
then的微任务特性:resolve触发后,then回调会被加入微任务队列,等待当前宏任务执行完后执行 —— 这是“异步执行回调”,而非“异步执行 IO”。
-
是否阻塞:判断是否阻塞的核心是「耗时操作是否占用主线程」,而非「回调是否异步」。fs.readFile的IO操作在底层线程池,因此无论是否封装Promise,都不阻塞。
补充:容易混淆的“同步/异步”点。
| Promise执行器函数。 | 同步。 | 仅执行“发起请求”,不阻塞。 | 主线程同步代码。 |
| fs.readFile。 | 异步IO。 | 不阻塞(底层线程池处理)。 | IO回调宏任务。 |
| fs.readFileSync。 | 同步IO。 | 阻塞(主线程处理)。 | 主线程同步代码。 |
| Promise.then回调。 | 异步。 | 不阻塞(微任务队列)。 | 微任务。 |
| setTimeout回调。 | 异步。 | 不阻塞(定时器宏任务)。 | 宏任务。 |





