第4章 4.2 Promise 链式调用
🎯 开场:为什么你会被回调地狱卡住?
上一章我们学了 Callback 回调函数,用它来处理「等数据回来再执行」的场景。听起来挺美好的对吧?
但现实是这样的:
你写了个获取用户信息的函数,等用户信息返回后再获取他的朋友圈列表,然后等朋友圈返回后再获取第一条动态的评论……
结果代码变成了这样:
getUser(userId, function(user) {
getFriends(user.id, function(friends) {
getFirstPost(friends[0], function(post) {
getComments(post.id, function(comments) {
console.log(comments);
});
});
});
});
恭喜你,你已经掉进了 回调地狱(Callback Hell)!
这代码看着就让人头皮发\n\n
\n\n
\n\n麻,更别提后期维护和 Debug 了。而且更坑爹的是:错误处理散落在每一层,一旦某个环节出错了,你根本不知道是哪一层的问题。
学完这一章,你能:
- 理解 Promise 是什么,为什么它能解决回调地狱
- 掌握 .then() .catch() .finally() 的链式写法
- 写出优雅的、像流水线一样流畅的异步代码
🧱 基础:Promise 是个什么"承诺"?
4.2.1 生活类比:点外卖
想象一下你点了一份外卖:
- 你下单了(发起请求)
- 店家接单并告诉你「30分钟后送达」(Promise 进入 pending 状态)
- 30分钟后,外卖到了,你拿到外卖开始吃(resolved)
- 或者,30分钟后店家打电话说「不好意思,卖完了」(rejected)
Promise 就是这么个"承诺":它代表一个异步操作的最终结果——要么成功,要么失败。
4.2.2 创建你的第一个 Promise
// 创建一个 Promise
const promise = new Promise(function(resolve, reject) {
// 模拟异步操作:2秒后告诉你"饭到了"
setTimeout(function() {
const 饭好了 = true;
if (饭好了) {
resolve("宫保鸡丁来啦!"); // 成功了,传递结果
} else {
reject("不好意思,菜卖完了"); // 失败了,传递错误原因
}
}, 2000);
});
console.log("我刚下单,正在等待...");
// 等待 Promise 完成
promise.then(function(结果) {
console.log("成功:" + 结果);
}).catch(function(错误) {
console.log("失败:" + 错误);
});
运行结果:
我刚下单,正在等待...
(等2秒)
成功:宫保鸡丁来啦!
new Promise() 接受一个函数,这个函数有两个参数:resolve(成功时调用)和 reject(失败时调用)。.then() 捕获成功结果,.catch() 捕获失败原因。
4.2.3 链式调用的精髓
Promise 的真正强大之处在于:.then() 返回的还是一个 Promise,所以你可以一直 .then() 下去:
// 模拟:获取用户 → 获取订单 → 获取物流
function 获取用户() {
return new Promise(function(resolve) {
setTimeout(function() {
resolve({ 用户名: "张三", id: 100 });
}, 500);
});
}
function 获取订单(用户) {
return new Promise(function(resolve) {
setTimeout(function() {
resolve({ 订单号: "DD2024001", 用户Id: 用户.id });
}, 500);
});
}
function 获取物流(订单) {
return new Promise(function(resolve) {
setTimeout(function() {
resolve({ 物流单号: "SF10086", 状态: "运输中" });
}, 500);
});
}
// 链式调用!像流水线一样清晰
获取用户()
.then(function(用户) {
console.log("第1步:拿到用户信息", 用户);
return 获取订单(用户); // 返回新的 Promise
})
.then(function(订单) {
console.log("第2步:拿到订单信息", 订单);
return 获取物流(订单); // 返回新的 Promise
})
.then(function(物流) {
console.log("第3步:拿到物流信息", 物流);
})
.catch(function(错误) {
console.log("中间任何一步出错都会被捕获:", 错误);
});
运行结果:
第1步:拿到用户信息 { 用户名: '张三', id: 100 }
第2步:拿到订单信息 { 订单号: 'DD2024001', 用户Id: 100 }
第3步:拿到物流信息 { 物流单号: 'SF10086', 状态: '运输中' }
关键点:.then() 里的 return 语句把数据传给下一个 .then()。没有 return,下一个 .then() 收到的就是 undefined。
4.2.4 Promise.resolve 和 Promise.reject:快捷方式
有时候你只是想快速创建一个「已经完成」或「已经失败」的 Promise:
// 快速创建一个已成功的 Promise
Promise.resolve("直接成功").then(function(结果) {
console.log(结果); // 直接成功
});
// 快速创建一个已失败的 Promise
Promise.reject("直接失败").catch(function(错误) {
console.log(错误); // 直接失败
});
// 更有用的场景:把一个普通值转成 Promise
const 用户数据 = { 姓名: "李四", 年龄: 25 };
Promise.resolve(用户数据).then(function(用户) {
console.log(用户.姓名); // 李四
});
4.2.5 finally:无论成功失败都执行
function 加载数据() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
const 成功 = Math.random() > 0.5; // 随机决定成功或失败
if (成功) {
resolve("数据加载成功!");
} else {
reject("加载失败了...");
}
}, 1000);
});
}
// 显示/隐藏加载动画的典型用法
加载数据()
.then(function(结果) {
console.log(结果);
})
.catch(function(错误) {
console.log(错误);
})
.finally(function() {
console.log("不管成功还是失败,我都会执行,通常用于隐藏加载动画");
});
🔥 实战:3 个递进小项目
项目 1:5 分钟 - 天气查询流水线(理解核心 API)
场景:按顺序查询「城市 → 经纬度 → 天气」
// 模拟 API 调用
function 查询城市(城市名) {
return new Promise(function(resolve) {
setTimeout(function() {
const 数据库 = {
"北京": { 城市名: "北京", 城市代码: "BJ" },
"上海": { 城市名: "上海", 城市代码: "SH" },
"杭州": { 城市名: "杭州", 城市代码: "HZ" }
};
const 结果 = 数据库[城市名] || null;
resolve(结果);
}, 500);
});
}
function 查询经纬度(城市) {
return new Promise(function(resolve) {
setTimeout(function() {
const 经纬度表 = {
"BJ": { 纬度: 39.9, 经度: 116.4 },
"SH": { 纬度: 31.2, 经度: 121.5 },
"HZ": { 纬度: 30.3, 经度: 120.2 }
};
resolve({
城市名: 城市.城市名,
...经纬度表[城市.城市代码]
});
}, 500);
});
}
function 查询天气(位置) {
return new Promise(function(resolve) {
setTimeout(function() {
resolve({
城市: 位置.城市名,
温度: Math.floor(Math.random() * 15) + 15, // 15-30度随机
天气: ["晴", "多云", "小雨"][Math.floor(Math.random() * 3)]
});
}, 500);
});
}
// 查询杭州的天气
查询城市("杭州")
.then(function(城市) {
if (!城市) throw new Error("城市不存在");
return 查询经纬度(城市);
})
.then(function(位置) {
return 查询天气(位置);
})
.then(function(天气) {
console.log("查询结果:", 天气);
})
.catch(function(错误) {
console.log("出错了:", 错误.message);
});
预期输出:
查询结果: { 城市: '杭州', 纬度: 30.3, 经度: 120.2, 温度: 22, 天气: '多云' }
一句话解释:Promise 链就像一条流水线,每个 .then() 处理一步,完成后把结果传给下一步。
项目 2:15 分钟 - 读取 CSV 文件并处理数据
场景:读取用户数据 CSV → 过滤 → 转换格式 → 输出
// 模拟从文件或 API 获取的 CSV 数据
const 用户CSV数据 = `id,姓名,年龄,城市
1001,王五,28,北京
1002,赵六,35,上海
1003,钱七,22,杭州
1004,孙八,31,北京
1005,周九,27,深圳`;
// 解析 CSV
function 解析CSV(csv文本) {
return new Promise(function(resolve) {
setTimeout(function() {
const 行数组 = csv文本.trim().split("\n");
const 表头 = 行数组[0].split(",");
const 数据 = 行数组.slice(1).map(function(行) {
const 值 = 行.split(",");
return {
id: 值[0],
姓名: 值[1],
年龄: parseInt(值[2]),
城市: 值[3]
};
});
resolve(数据);
}, 300);
});
}
// 过滤:只保留年龄大于 25 岁的
function 过滤成年人(用户列表) {
return new Promise(function(resolve) {
setTimeout(function() {
resolve(用户列表.filter(function(用户) {
return 用户.年龄 > 25;
}));
}, 300);
});
}
// 转换:给每个用户加一个"标签"
function 添加标签(用户列表) {
return new Promise(function(resolve) {
setTimeout(function() {
resolve(用户列表.map(function(用户) {
let 标签 = "普通用户";
if (用户.年龄 > 30) 标签 = "资深用户";
if (用户.城市 === "北京") 标签 = "北京" + 标签;
return { ...用户, 标签 };
}));
}, 300);
});
}
// 按城市分组
function 按城市分组(用户列表) {
return new Promise(function(resolve) {
const 分组结果 = {};
用户列表.forEach(function(用户) {
if (!分组结果[用户.城市]) {
分组结果[用户.城市] = [];
}
分组结果[用户.城市].push(用户);
});
resolve(分组结果);
});
}
// 执行流水线
解析CSV(用户CSV数据)
.then(function(用户列表) {
console.log("1. 解析完成,共", 用户列表.length, "条数据");
return 过滤成年人(用户列表);
})
.then(function(成年人列表) {
console.log("2. 过滤后,25岁以上有", 成年人列表.length, "人");
return 添加标签(成年人列表);
})
.then(function(带标签列表) {
console.log("3. 添加标签完成");
return 按城市分组(带标签列表);
})
.then(function(分组结果) {
console.log("4. 按城市分组结果:");
console.log(JSON.stringify(分组结果, null, 2));
})
.catch(function(错误) {
console.log("处理失败:", 错误.message);
});
预期输出:
1. 解析完成,共 5 条数据
2. 过滤后,25岁以上有 4 人
3. 添加标签完成
4. 按城市分组结果:
{
"北京": [
{ "id": "1001", "姓名": "王五", "年龄": 28, "城市": "北京", "标签": "北京普通用户" },
{ "id": "1004", "姓名": "孙八", "年龄": 31, "城市": "北京", "标签": "北京资深用户" }
],
"上海": [
{ "id": "1002", "姓名": "赵六", "年龄": 35, "城市": "上海", "标签": "资深用户" }
],
"杭州": [
{ "id": "1005", "姓名": "周九", "年龄": 27, "城市": "深圳", "标签": "普通用户" }
]
}
一句话解释:Promise 链让我们把「解析→过滤→转换→分组」四步清晰地串联起来,每一步都职责单一。
项目 3:15 分钟 - 待办事项管理小工具
场景:模拟一个待办清单的增删改查操作
// 模拟的本地存储
const 数据库 = {
待办列表: []
};
// 模拟网络延迟的辅助函数
function 延迟(毫秒) {
return new Promise(function(resolve) {
setTimeout(resolve, 毫秒);
});
}
// 添加待办
function 添加待办(内容) {
return new Promise(function(resolve) {
延迟(300).then(function() {
const 新待办 = {
id: Date.now(),
内容: 内容,
完成: false,
创建时间: new Date().toLocaleString()
};
数据库.待办列表.push(新待办);
resolve(新待办);
});
});
}
// 标记完成
function 标记完成(id) {
return new Promise(function(resolve, reject) {
延迟(200).then(function() {
const 待办 = 数据库.待办列表.find(function(t) {
return t.id === id;
});
if (!待办) {
reject(new Error("待办不存在"));
return;
}
待办.完成 = true;
resolve(待办);
});
});
}
// 删除待办
function 删除待办(id) {
return new Promise(function(resolve, reject) {
延迟(200).then(function() {
const 索引 = 数据库.待办列表.findIndex(function(t) {
return t.id === id;
});
if (索引 === -1) {
reject(new Error("待办不存在"));
return;
}
数据库.待办列表.splice(索引, 1);
resolve("删除成功");
});
});
}
// 查看所有待办
function 查看所有() {
return new Promise(function(resolve) {
延迟(100).then(function() {
resolve([...数据库.待办列表]);
});
});
}
// 演示:添加3个待办,标记第一个完成,删除第二个
console.log("=== 待办清单演示 ===");
添加待办("学习 Promise 链式调用")
.then(function(待办) {
console.log("✅ 添加成功:", 待办.内容);
return 添加待办("写一个天气查询程序");
})
.then(function(待办) {
console.log("✅ 添加成功:", 待办.内容);
return 添加待办("吃饭");
})
.then(function(待办) {
console.log("✅ 添加成功:", 待办.内容);
return 查看所有();
})
.then(function(列表) {
console.log("\n📋 当前所有待办:", 列表.length, "条");
列表.forEach(function(t) {
console.log(" -", t.id, t.内容, t.完成 ? "✓" : "○");
});
// 返回第一个待办的 id 用于后续操作
return 列表[0].id;
})
.then(function(第一个待办Id) {
console.log("\n✏️ 标记第一个待办为完成...");
return 标记完成(第一个待办Id);
})
.then(function(待办) {
console.log("✅ 已完成:", 待办.内容);
return 查看所有();
})
.then(function(列表) {
console.log("\n📋 当前状态:");
列表.forEach(function(t) {
console.log(" -", t.id, t.内容, t.完成 ? "✓" : "○");
});
// 返回第二个待办的 id 用于删除
return 列表[1].id;
})
.then(function(第二个待办Id) {
console.log("\n🗑️ 删除第二个待办...");
return 删除待办(第二个待办Id);
})
.then(function(消息) {
console.log("✅", 消息);
return 查看所有();
})
.then(function(列表) {
console.log("\n📋 最终待办列表:", 列表.length, "条");
列表.forEach(function(t) {
console.log(" -", t.id, t.内容, t.完成 ? "✓" : "○");
});
})
.catch(function(错误) {
console.log("❌ 错误:", 错误.message);
})
.finally(function() {
console.log("\n=== 演示结束 ===");
});
预期输出:
=== 待办清单演示 ===
✅ 添加成功: 学习 Promise 链式调用
✅ 添加成功: 写一个天气查询程序
✅ 添加成功: 吃饭
📋 当前所有待办: 3 条
- 1719123456789 学习 Promise 链式调用 ○
- 1719123456790 写一个天气查询程序 ○
- 1719123456791 吃饭 ○
✏️ 标记第一个待办为完成...
✅ 已完成: 学习 Promise 链式调用
📋 当前状态:
- 1719123456789 学习 Promise 链式调用 ✓
- 1719123456790 写一个天气查询程序 ○
- 1719123456791 吃饭 ○
🗑️ 删除第二个待办...
✅ 删除成功
📋 最终待办列表: 2 条
- 1719123456789 学习 Promise 链式调用 ✓
- 1719123456791 吃饭 ○
=== 演示结束 ===
一句话解释:用 Promise 链模拟了一个完整的 CRUD 操作,代码读起来就像在读一个「先做A,再做B,最后做C」的故事情节。
💪 进阶:常见坑 + 调试技巧
坑 1:忘记了 return
// ❌ 错误写法:没有 return,下一个 then 收不到结果
fetchUser()
.then(function(用户) {
console.log(用户);
fetchOrders(用户.id); // 没有 return!
})
.then(function(订单) {
console.log(订单); // 这里拿到的是 undefined
});
// ✅ 正确写法
fetchUser()
.then(function(用户) {
console.log(用户);
return fetchOrders(用户.id); // 记得 return
})
.then(function(订单) {
console.log(订单); // 正常拿到订单
});
坑 2:以为 Promise 会"等"你
// ❌ 错误理解:以为 this 会在 2 秒后变红
const 状态 = { 颜色: "白" };
new Promise(function(resolve) {
setTimeout(function() {
状态.颜色 = "红";
resolve();
}, 2000);
});
console.log(状态.颜色); // 立即输出"白",不会等2秒!
// ✅ 正确做法:用 Promise 封装你要等待的操作
function 等2秒变红() {
return new Promise(function(resolve) {
setTimeout(function() {
状态.颜色 = "红";
resolve(状态);
}, 2000);
});
}
等2秒变红().then(function(状态) {
console.log(状态.颜色); // 等2秒后输出"红"
});
坑 3:忘记 catch 错误
// ❌ 危险写法:没有任何错误处理
fetchData()
.then(function(data) {
return processData(data);
})
.then(function(result) {
console.log(result);
});
// 如果 fetchData 或 processData 出错,程序会崩溃但你不知道原因
// ✅ 正确写法:至少要有一个 catch
fetchData()
.then(function(data) {
return processData(data);
})
.then(function(result) {
console.log(result);
})
.catch(function(错误) {
console.error("出错了:", 错误.message);
});
坑 4:链式调用中 return 的是普通值还是 Promise?
// ✅ 两种写法都可以,Promise.resolve 会帮你包装
Promise.resolve(123)
.then(function(数字) {
return 数字 * 2; // 返回普通值,会被自动转成 Promise
})
.then(function(结果) {
console.log(结果); // 246
});
// 或者明确返回一个 Promise
Promise.resolve(123)
.then(function(数字) {
return Promise.resolve(数字 * 2); // 明确返回 Promise
})
.then(function(结果) {
console.log(结果); // 246
});
坑 5:Promise.all 有一个失败就全失败
// ❌ 错误理解:以为 Promise.all 要全部成功才执行
Promise.all([
fetchUser(1), // 成功了
fetchUser(999) // 失败了,id 不存在
]).then(function(结果) {
console.log("不会执行到这里");
});
// ✅ 正确做法:用 Promise.allSettled 捕获所有结果
Promise.allSettled([
fetchUser(1),
fetchUser(999)
]).then(function(结果) {
console.log("不管成功失败,都会到这里");
结果.forEach(function(单项) {
if (单项.status === "fulfilled") {
console.log("成功:", 单项.value);
} else {
console.log("失败:", 单项.reason.message);
}
});
});
调试技巧:用 .then 打印中间状态
// 简单粗暴的调试方式:在关键节点打印
fetchData()
.then(function(data) {
console.log("步骤1完成,数据是:", data); // 调试打印
return processData(data);
})
.then(function(result) {
console.log("步骤2完成,结果是:", result); // 调试打印
return saveResult(result);
})
.then(function(保存结果) {
console.log("步骤3完成,最终结果:", 保存结果);
})
.catch(function(错误) {
console.error("任何步骤出错:", 错误);
});
✏️ 练习题
练习 1(2 分钟):改造天气查询
- 输入:把城市名从"杭州"改成"北京"
- 预期输出:显示北京的天气信息
- 提示:只改一个变量的值
练习 2(3 分钟):添加判断
- 输入:在天气查询项目中,加一个判断,气温低于 10 度显示"记得穿秋裤"
- 预期输出:气温<10度时额外打印这条提示
- 提示:用 if (天气.温度 < 10) 在最后一个 .then() 里做判断
练习 3(5 分钟):处理新数据
- 输入:用项目 2 的方法处理以下 CSV:
id,姓名,城市
1,小明,北京
2,小红,上海
3,小刚,北京
- 预期输出:按城市分组后的 JSON
- 提示:CSV 结构变了,解析函数需要调整
split(",")的位置
练习 4(8 分钟):串接两个项目
- 输入:用项目 2 的 CSV 处理逻辑 + 项目 3 的待办逻辑,实现「从 CSV 读取用户,给每个用户创建一个待办」
- 预期输出:创建成功 N 条待办
- 提示:.then() 里的 map 结合 Promise.all
练习 5(5 分钟):分析报错
- 输入:以下代码运行后会输出什么?为什么?
Promise.resolve("a")
.then(function(x) {
console.log(x);
return Promise.resolve("b");
})
.then(function(x) {
console.log(x);
});
Promise.resolve("c")
.then(function(x) {
console.log(x);
});
- 预期输出:自己运行看看,然后解释为什么输出顺序是这样的
- 提示:JavaScript 是单线程的,Promise 微任务的执行顺序
作业:做一个「Promise 链式调用实战工具」
做一个成绩查询系统,模拟真实的异步数据处理流程:
- 需求描述:读取学生成绩 CSV → 计算平均分 → 按等级分类 → 输出报表
- 功能点:
1. CSV 格式:姓名,语文,数学,英语
2. 计算每个学生的平均分
3. 按「优秀(>=90)/良好(>=80)/及格(>=60)/不及格(<60)」分类
4. 输出每个等级的 学生姓名和平均分 - 加分项:
1. 用Promise.all并行处理多个学生的计算
2. 添加错误处理(比如分数超出 0-100 范围) - 验收标准:能跑起来 + 输出格式清晰 + 代码有注释
- 提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
本文学了 3 个核心点:
1. Promise 是个「承诺」,代表异步操作的最终结果
2. .then().then().catch() 链式调用让异步代码像流水线一样清晰
3. 每个 .then() 里记得 return,否则数据传不下去
延伸学习资源:
- MDN 官方文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise
- 《你不知道的 JavaScript(上卷)》第 3 章:Promise 基础
- 视频:Chrome 开发者工具里调试 Promise 的技巧
互动钩子:
你在项目里有没有遇到过「回调地狱」的糟心经历?后来是怎么解决的?评论区聊聊,老粉优先回复!
📢 下章预告:这一章我们学了 Promise 链式调用,但写起来还是有一丢丢繁琐。下一章我们要学习一个让异步代码「看起来像同步代码」的神奇语法——async/await,学完之后你会发现异步编程变得 so easy!

评论(0)