第3章 3.2 Promise 基础

前置章节回顾

上一章我们被「回调地狱」折磨得不轻——层层嵌套的回调函数让代码变成了一团乱麻,改一行代码都要找半天。你还记得那个「查天气 → 查衣服 → 查出门建议」的噩梦吗?

本章目标

这一章,我们要学一个新武器:Promise。它能把「层层嵌套」的回调拍平,变成「链式书写」,让代码从右往左金字塔变成从左往右流水线。学完本文,你就能自己写出「等数据来了再处理」的逻辑,而且代码整整洁洁。

配图说明:回调地狱 vs Promise 链式调用对比图 配图1 - 配图1


🎯 开场 3 分钟:为什么要学这个?

场景引入:等外卖的焦虑

想象你点了外卖,APP 上显示「正在等待骑手接单」,你每隔 10 秒就打开 APP 看一次——累不累?

后来 APP 出了个功能:「接单后自动通知你」,你就可以安心刷视频了,等通知来了再去拿。

回调函数就像你不停地打开 APP 刷新,Promise 就像那个「自动通知」功能。

现实开发中的痛点

做 Web 开发时,你肯定遇到过这种代码:

// 读取用户信息
getUser(userId, function(user) {
// 根据用户查订单
getOrders(user.id, function(orders) {
    // 根据订单查商品详情
    getProducts(orders, function(products) {
        // 根据商品查库存
        getStock(products, function(stock) {
            console.log('最终结果:', stock);
        });
    });
});
});

每一步都要等上一步完成才能开始,而且如果中途任何一步出错……恭喜你,debug 去吧。

学完本文能解决

  • ✅ 把「金字塔」代码变成「流水线」代码
  • ✅ 优雅地处理「成功」和「失败」两种情况
  • ✅ 学会 Promise 的链式调用,让代码易读易维护

🧱 基础 25 分钟:核心概念

3.2.1 Promise 是什么?

生活类比:Promise就像餐厅的「取餐号牌」。

你点完餐,服务员给你一个号牌,告诉你「做好了会响,你不用在这干等着」。你拿到号牌后可以玩手机、聊天,等号牌响了去取餐就行。

  • 号牌本身:就是 Promise 对象
  • 「做好了」:就是 Promise 变成 fulfilled 状态(成功)
  • 「没货了」:就是 Promise 变成 rejected 状态(失败)

3.2.2 为什么用 Promise?

解决啥痛点:回调函数的问题在于「不确定性」——你不知道上一步什么时候完成,只能被动等待或者层层嵌套。

Promise 提供了「约定」:我保证最终会给你一个结果(成功或失败),你只需要 .then() 注册一下怎么处理。

配图说明:Promise 三种状态流转图 配图2 - 配图2

3.2.3 怎么用?最小代码演示

第一步:创建一个 Promise

// 创建 Promise,接收一个函数作为参数
// 这个函数有两个参数:resolve 和 reject
const myFirstPromise = new Promise(function(resolve, reject) {
// 模拟一个异步操作(比如读文件、网络请求)
setTimeout(function() {
    const 成功 = true;
    if (成功) {
        resolve('数据加载完成!');  // 成功了,调用 resolve
    } else {
        reject('网络错误');          // 失败了,调用 reject
    }
}, 1000);  // 1秒后执行
});

console.log('我不需要等,直接执行到这里');

输出:

我不需要等,直接执行到这里
(等待1秒后)
数据加载完成!

解释:Promise 创建后立即返回,不会阻塞后面代码。1秒后「异步操作」完成,才调用 resolvereject

第二步:处理结果(then / catch / finally)

myFirstPromise
.then(function(结果) {
    console.log('成功啦:', 结果);
})
.catch(function(错误) {
    console.log('失败啦:', 错误);
})
.finally(function() {
    console.log('不管成功还是失败,我都会执行');
});

解释
- .then() 注册「成功后怎么办」
- .catch() 注册「失败后怎么办」
- .finally() 注册「不管成功失败都执行啥」

第三步:resolve 和 reject 能传值

const 查成绩 = new Promise(function(resolve, reject) {
const 分数 = 85;
if (分数 >= 60) {
    resolve({ success: true, score: 分数 });
} else {
    reject({ success: false, reason: '没及格' });
}
});

查成绩
.then(function(data) {
    console.log('成绩单:', data);  // { success: true, score: 85 }
})
.catch(function(data) {
    console.log('挂了:', data);    // 如果没及格就执行这里
});

解释resolvereject 可以传任意值,这个值会被 .then().catch() 收到。


3.2.4 链式调用——Promise 的精髓

Promise 最厉害的地方:.then() 返回的还是 Promise,可以继续 .then()

// 模拟:步骤1:登录 -> 步骤2:获取用户信息 -> 步骤3:获取用户权限
const 登录 = function(用户名) {
return new Promise(function(resolve) {
    setTimeout(function() {
        resolve({ userId: 123, name: 用户名 });
    }, 500);
});
};

const 获取权限 = function(用户ID) {
return new Promise(function(resolve) {
    setTimeout(function() {
        resolve(['read', 'write', 'delete']);
    }, 500);
});
};

// 链式调用:登录成功后获取用户信息,获取成功后获取权限
登录('小明')
.then(function(用户) {
    console.log('登录成功,用户信息:', 用户);
    return 获取权限(用户.userId);  // 返回新的 Promise
})
.then(function(权限列表) {
    console.log('权限列表:', 权限列表);
});

输出:

登录成功,用户信息:{ userId: 123, name: '小明' }
权限列表:['read', 'write', 'delete']

解释:每个 .then()return 一个值或 Promise,下一个 .then() 就能接收到。这就是「流水线」的威力。


3.2.5 Promise.all——并行执行,一次性拿到所有结果

场景:页面加载时要同时查「用户信息」「订单列表」「推荐商品」,三个请求互不相干,等三个都完成了再一起处理。

const 查用户 = new Promise(function(resolve) {
setTimeout(function() { resolve({ name: '小明', age: 18 }); }, 500);
});

const 查订单 = new Promise(function(resolve) {
setTimeout(function() { resolve(['订单A', '订单B']); }, 800);
});

const 查推荐 = new Promise(function(resolve) {
setTimeout(function() { resolve(['商品1', '商品2']); }, 600);
});

// Promise.all:所有 Promise 都成功,才算成功
Promise.all([查用户, 查订单, 查推荐])
.then(function(结果列表) {
    console.log('用户:', 结果列表[0]);
    console.log('订单:', 结果列表[1]);
    console.log('推荐:', 结果列表[2]);
})
.catch(function(错误) {
    console.log('有一个请求失败了:', 错误);
});

输出:

用户:{ name: '小明', age: 18 }
订单:['订单A', '订单B']
推荐:['商品1', '商品2']

解释Promise.all() 等待所有 Promise 完成,输出顺序和输入顺序一致。耗时 = 最慢那个(约800ms),而不是加起来。


🔥 实战 35 分钟:3 个递进小项目

项目 1(5分钟):计时器 Promise

需求:写一个「等 X 毫秒后自动完成」的 Promise 工具函数。

完整代码

// 等待指定毫秒数后自动 resolve
function 等待(毫秒) {
return new Promise(function(resolve) {
    setTimeout(resolve, 毫秒);
});
}

console.log('开始计时...');
等待(2000)  // 等待2秒
.then(function() {
    console.log('2秒到!');
    return 等待(1000);  // 再等1秒
})
.then(function() {
    console.log('又过了1秒,总共3秒了');
});

预期输出

开始计时...
(等待2秒)
2秒到!
(等待1秒)
又过了1秒,总共3秒了

一句话解释等待() 函数返回一个 Promise,.then() 注册的回调会在指定时间后自动执行。


项目 2(15分钟):CSV 数据处理流水线

需求:从模拟的 CSV 数据中筛选「年龄大于18岁」的用户,并统计人数。

数据格式(模拟 CSV 字符串):

name,age,city
小明,15,北京
老王,25,上海
小红,17,广州
张三,30,深圳
李四,19,成都

完整代码

// 模拟的 CSV 数据
const csv数据 = `name,age,city
小明,15,北京
老王,25,上海
小红,17,广州
张三,30,深圳
李四,19,成都`;

// 把 CSV 字符串解析成对象数组
function 解析CSV(csv字符串) {
return new Promise(function(resolve, reject) {
    try {
        const 行列表 = csv字符串.trim().split('\n');
        const 表头 = 行列表[0].split(',');
        const 数据 = 行列表.slice(1).map(function(行) {
            const 值列表 = 行.split(',');
            return {
                name: 值列表[0],
                age: parseInt(值列表[1]),
                city: 值列表[2]
            };
        });
        resolve(数据);
    } catch (错误) {
        reject('CSV解析失败:' + 错误.message);
    }
});
}

// 筛选年龄大于指定值的数据
function 筛选年龄大于(数据, 最小年龄) {
return new Promise(function(resolve) {
    const 结果 = 数据.filter(function(用户) {
        return 用户.age > 最小年龄;
    });
    resolve(结果);
});
}

// 统计并输出结果
function 统计输出(数据) {
return new Promise(function(resolve) {
    console.log('=== 筛选结果 ===');
    console.log('符合条件的人数:', 数据.length);
    数据.forEach(function(用户) {
        console.log('- ' + 用户.name + ',' + 用户.age + '岁,住' + 用户.city);
    });
    resolve(数据.length);
});
}

// 执行流水线
解析CSV(csv数据)
.then(function(数据) {
    console.log('解析成功,共' + 数据.length + '条记录');
    return 筛选年龄大于(数据, 18);  // 筛选18岁以上的
})
.then(function(筛选后数据) {
    return 统计输出(筛选后数据);
})
.then(function(人数) {
    console.log('=== 任务完成 ===');
})
.catch(function(错误) {
    console.log('出错了:', 错误);
});

预期输出

解析成功,共5条记录
=== 筛选结果 ===
符合条件的人数:3
- 老王,25岁,住上海
- 张三,30岁,住深圳
- 李四,19岁,住成都
=== 任务完成 ===

一句话解释:把 CSV 解析 → 数据筛选 → 结果输出串成一条流水线,每个步骤都是独立的 Promise,职责清晰。


项目 3(15分钟):待办事项管理器

需求:做一个命令行待办清单,支持「添加」「完成」「查看未完成」三个操作。

完整代码

const readline = require('readline');

// 模拟数据库(内存存储)
const 数据库 = {
待办列表: [
    { id: 1, 内容: '买牛奶', 已完成: false },
    { id: 2, 内容: '写周报', 已完成: false },
    { id: 3, 内容: '健身', 已完成: true }
],
下一个ID: 4
};

// 模拟数据库操作的 Promise 封装
const 查询所有 = function() {
return new Promise(function(resolve) {
    setTimeout(function() {
        resolve(数据库.待办列表);
    }, 100);
});
};

const 添加 = function(内容) {
return new Promise(function(resolve) {
    setTimeout(function() {
        const 新任务 = {
            id: 数据库.下一个ID++,
            内容: 内容,
            已完成: false
        };
        数据库.待办列表.push(新任务);
        resolve(新任务);
    }, 100);
});
};

const 标记完成 = function(任务ID) {
return new Promise(function(resolve, reject) {
    setTimeout(function() {
        const 任务 = 数据库.待办列表.find(function(t) {
            return t.id === 任务ID;
        });
        if (任务) {
            任务.已完成 = true;
            resolve(任务);
        } else {
            reject('任务ID不存在:' + 任务ID);
        }
    }, 100);
});
};

const 筛选未完成 = function() {
return new Promise(function(resolve) {
    setTimeout(function() {
        const 结果 = 数据库.待办列表.filter(function(t) {
            return !t.已完成;
        });
        resolve(结果);
    }, 100);
});
};

// 显示待办列表
function 显示列表(列表) {
console.log('\n========== 待办清单 ==========');
if (列表.length === 0) {
    console.log('(空)');
} else {
    列表.forEach(function(任务) {
        const 状态 = 任务.已完成 ? '✓' : '○';
        const 前缀 = 任务.已完成 ? '[完成]' : '[待办]';
        console.log(前缀 + ' ' + 任务.id + '. ' + 任务.内容 + ' ' + 状态);
    });
}
console.log('==============================\n');
}

// ========== 主程序 ==========

// 演示流程
console.log('欢迎使用待办清单管理器!\n');

// 1. 查看当前列表
查询所有()
.then(function(列表) {
    显示列表(列表);
    return 筛选未完成();
})
// 2. 查看未完成
.then(function(未完成列表) {
    console.log('【未完成任务】');
    显示列表(未完成列表);
    return 添加('学习 Promise');
})
// 3. 添加新任务
.then(function(新任务) {
    console.log('已添加:' + 新任务.内容 + ' (ID=' + 新任务.id + ')');
    return 标记完成(1);
})
// 4. 标记任务1完成
.then(function(完成任务) {
    console.log('已标记完成:' + 完成任务.内容);
    return 筛选未完成();
})
// 5. 再次查看未完成
.then(function(未完成列表) {
    console.log('\n【更新后的未完成任务】');
    显示列表(未完成列表);
})
.catch(function(错误) {
    console.log('操作失败:', 错误);
});

预期输出

欢迎使用待办清单管理器!

========== 待办清单 ==========
[待办] 1. 买牛奶 ○
[完成] 3. 健身 ✓
[待办] 2. 写周报 ○
==============================

【未完成任务】
========== 待办清单 ==========
[待办] 1. 买牛奶 ○
[待办] 2. 写周报 ○
==============================

已添加:学习 Promise (ID=4)
已标记完成:买牛奶

【更新后的未完成任务】
========== 待办清单 ==========
[待办] 2. 写周报 ○
[待办] 4. 学习 Promise ○
==============================

一句话解释:每个数据库操作都封装成 Promise,主程序通过 .then() 链把操作串联起来,逻辑清晰。


💪 进阶 20 分钟:常见坑 + 性能小贴士

坑 1:忘记 return Promise

// ❌ 错误示例
fetch('/api/user')
.then(function(user) {
    fetch('/api/orders/' + user.id)  // 忘记 return
})
.then(function(orders) {
    console.log(orders);  // orders 是 undefined!因为上一步没返回
});

// ✅ 正确示例
fetch('/api/user')
.then(function(user) {
    return fetch('/api/orders/' + user.id);  // 必须 return
})
.then(function(orders) {
    console.log(orders);
});

解释.then() 里的代码要传给下一个 .then(),必须 return。没 return 就是 undefined


坑 2:在 .then() 里 throw 错误而不是 reject

// ❌ 错误示例
somePromise()
.then(function(data) {
    if (!data) {
        console.log('出错了');  // 只是打印,没有 reject
    }
    return data;
})
.catch(function() {
    console.log('这里捕获不到上面的错误!');
});

// ✅ 正确示例
somePromise()
.then(function(data) {
    if (!data) {
        throw new Error('数据为空');  // 用 throw 触发 catch
    }
    return data;
})
.catch(function(错误) {
    console.log('捕获到错误:', 错误.message);
});

解释.catch() 只能捕获被 reject()throw 抛出的错误。普通 console.log 不算。


坑 3:Promise.all 里有一个失败就全失败

// ❌ 错误示例
Promise.all([
fetch('/api/a'),
fetch('/api/b'),
fetch('/api/c')
]).then(function(结果) {
// 如果任何一个请求失败,这里不会执行
});

// ✅ 正确示例:全部 resolve 才算成功
// 如果需要「容忍部分失败」,用 Promise.allSettled
Promise.allSettled([
fetch('/api/a'),
fetch('/api/b'),
fetch('/api/c')
]).then(function(结果列表) {
结果列表.forEach(function(结果) {
    if (结果.status === 'fulfilled') {
        console.log('成功:', 结果.value);
    } else {
        console.log('失败:', 结果.reason);
    }
});
});

解释Promise.all 是「与」的关系,有一个失败整体就失败。Promise.allSettled 是「不管成功失败都返回」。


坑 4:回调里又用回调,没有真正用 Promise

// ❌ 错误示例:表面用 Promise,实际还是回调嵌套
function 假Promise(回调) {
setTimeout(function() {
    if (Math.random() > 0.5) {
        回调(null, '成功');
    } else {
        回调('失败', null);
    }
}, 100);
}

// ✅ 正确示例:真正返回 Promise
function 真Promise() {
return new Promise(function(resolve, reject) {
    setTimeout(function() {
        if (Math.random() > 0.5) {
            resolve('成功');
        } else {
            reject('失败');
        }
    }, 100);
});
}

解释:有些老代码把回调函数叫「callback」,但并不是 Promise。Promise 一定是 new Promise() 返回的对象。


坑 5:忘记处理 reject

// ❌ 危险示例:没有任何错误处理
fetch('/api/data')
.then(function(data) {
    console.log(data);
});
// 如果 fetch 失败,没有任何提示,静默失败

// ✅ 正确示例:至少要加 catch
fetch('/api/data')
.then(function(data) {
    console.log(data);
})
.catch(function(错误) {
    console.error('请求失败:', 错误);
});

解释:没有 .catch() 的 Promise 就像没有 try-catch 的代码,出了错都不知道。


性能小贴士:避免不必要的 await

// ❌ 低效:串行执行,耗时 = A + B + C
async function 串行获取() {
const a = await 获取A();
const b = await 获取B();
const c = await 获取C();
return [a, b, c];
}

// ✅ 高效:并行执行,耗时 = max(A, B, C)
async function 并行获取() {
const [a, b, c] = await Promise.all([
    获取A(),
    获取B(),
    获取C()
]);
return [a, b, c];
}

解释:如果多个请求之间没有依赖,不要串行 await,用 Promise.all() 并行执行,速度快很多。


调试技巧:Promise 链里打日志

// 小技巧:每个 .then() 前后打日志,方便定位问题
fetch('/api/user')
.then(function(user) {
    console.log('[调试] 获取用户成功:', user);
    return fetch('/api/orders/' + user.id);
})
.then(function(orders) {
    console.log('[调试] 获取订单成功:', orders.length, '条');
    return 加工订单(orders);
})
.catch(function(错误) {
    console.error('[调试] 发生错误:', 错误);
    throw 错误;  // 重新抛出,避免静默失败
})
.finally(function() {
    console.log('[调试] 请求流程结束');
});

解释:在关键节点加 [调试] 日志,出问题时一眼就能看到卡在哪一步。


✏️ 练习题 + 作业题

练习题(5道,10分钟内完成)

练习 1(2分钟):修改等待时间
- 输入:把项目1的等待时间从 2000 改成 3000
- 预期输出:2秒变成3秒后才输出"2秒到!"
- 提示:等待(毫秒) 函数的参数就是毫秒数

练习 2(2分钟):添加年龄筛选条件
- 输入:把项目2的年龄筛选从 >18 改成 >20
- 预期输出:符合条件人数从3人变成2人(老王和张三)
- 提示:找一个 18 改成 20

练习 3(2分钟):筛选城市
- 输入:在项目2基础上,新增一个 筛选城市(数据, 城市名) 函数
- 预期输出:调用 筛选城市(数据, '北京') 只返回住在北京的用户
- 提示:参考 筛选年龄大于 的写法,把 .filter() 条件改成 用户.city === 城市名

练习 4(2分钟):串联两个筛选
- 输入:先筛选年龄>18,再筛选城市是"上海"
- 预期输出:只有"老王"符合条件
- 提示:.then() 链里 return 另一个筛选函数的结果

练习 5(2分钟):分析报错
- 输入:下面代码运行后会输出什么?

new Promise(function(resolve, reject) {
reject('出错了');
})
.then(function() { console.log('第一个then'); })
.catch(function(e) { console.log('捕获:' + e); })
.then(function() { console.log('恢复后then'); });
  • 预期输出:捕获:出错了 然后 恢复后then
  • 提示:catch 后面的 .then() 还会继续执行

作业题(30分钟-2小时)

作业:做一个「数据验证流水线」

  • 需求描述:做一个命令行工具,输入用户注册信息,依次验证:用户名是否为空、密码长度是否>=6、两次密码是否一致
  • 功能点
    1. 用 Promise 封装每个验证步骤
    2. 验证失败时用 reject() 提示具体错误
    3. 验证通过后输出「注册成功」
  • 加分项
    1. 验证通过后用 Promise.all() 同时发送「发欢迎邮件」和「记录日志」两个模拟操作
    2. 给每个步骤加 [验证中] [成功] [失败] 日志
  • 验收标准
  • 能跑起来(Node.js 环境)
  • 输入错误数据能提示具体哪一步错
  • 输入正确数据能走完整个流程
  • 代码有适当注释
  • 提交方式:评论区贴代码或 GitHub 链接

📚 总结 + 资源

本章3个核心点

  • Promise 是什么:「取餐号牌」,承诺最终给结果,分 pending/fulfilled/rejected 三种状态
  • .then() 链式调用return 决定下一个 .then() 收到什么,实现流水线
  • Promise.all():并行执行多个 Promise,等全部成功才继续

延伸学习资源

  • MDN Promise 文档:权威、全面、中文友好
  • 《你不知道的 JavaScript》上卷第3章:深入讲解 Promise 原理
  • 视频:JavaScript Promise 慕课网免费教程(约1小时)

互动钩子

「你在实际项目里遇到过回调地狱吗?当时是怎么解决的?评论区聊聊,老粉优先回复!」


下章预告:Promise 链写起来已经很舒服了,但 async/await 让它更像「同步代码」。下一章我们学完就能把 .then().then().then() 变成看起来像「顺序执行」的神奇语法糖。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。