一、为什么需要 Promise?(背景)
JavaScript 是一门单线程语言,也就是说它一次只能执行一个任务。如果某个操作非常耗时(比如网络请求、文件读取),直接同步执行会阻塞整个页面,用户就什么都做不了了。
同步 vs 异步:
- 同步任务:按顺序执行,当前任务没完成,后面的代码就得等。
- 异步任务:交给浏览器其他线程去处理,完成后通过回调通知主线程继续执行。
console.log("1. 开始");
setTimeout(() => {
console.log("2. 定时器");
}, 1000);
console.log("3. 结束");
// 输出顺序:
// 1. 开始 → 3. 结束 → 2. 定时器
所以我们发现:异步任务不会阻塞主线程,但执行顺序和写代码的顺序不一致。
二、Promise 解决了什么问题?
在 Promise 出现之前,我们使用回调函数来处理异步任务,容易出现“回调地狱”(Callback Hell)。
🔍 回调地狱是什么?
当你有多个异步操作需要依次执行时,每个操作都需要等待前一个操作完成才能开始。由于 JavaScript 中的异步操作是通过回调函数实现的,这就导致了层层嵌套的回调函数结构,这种结构被称为“回调地狱”。
示例:回调地狱
doSomething(function(result) {
doNextThing(result, function(newResult) {
doAnotherThing(newResult, function(finalResult) {
// ...
});
});
});
- 难以阅读:随着嵌套层级增加,代码变得难以理解和维护。
- 错误处理复杂:每一层都需要单独处理错误,增加了代码量和出错概率。
- 扩展性差:添加新的逻辑或修改现有逻辑变得困难。
使用 Promise 改善回调地狱
Promise 就是为了解决这个问题而生的:
- 更清晰的流程控制
- 更好的错误处理
- 支持链式调用
三、Promise 的基本结构与状态
✅ 创建一个 Promise
const p = new Promise((resolve, reject) => {
// 这里写异步任务
setTimeout(() => {
const success = true;
if (success) {
resolve("成功啦!");
} else {
reject("失败啦!");
}
}, 1000);
});
📦 Promise 的三种状态:
| 状态 | 说明 |
|---|---|
| pending | 初始状态,未完成 |
| fulfilled | 成功,调用了 resolve() |
| rejected | 失败,调用了 reject() |
一旦状态变为 fulfilled 或 rejected,就不能再改变了。
四、then 和 catch 的作用
.then() 和 .catch() 是 Promise 提供的两个原型方法,用于处理结果或错误。
p.then(res => {
console.log("成功:", res);
}).catch(err => {
console.error("失败:", err);
});
你可以把它想象成:
.then():如果事情办成了,就执行这个;.catch():如果出错了,就在这里处理。
五、Promise 底层机制详解(简单模拟实现)
为了更好地理解 Promise 的底层工作原理,我们将手动实现一个极简版的 Promise。
🧩 Step 1:定义构造函数
function MyPromise(executor) {
let self = this;
self.status = "pending"; // 当前状态
self.value = undefined; // 成功的值
self.reason = undefined; // 失败的原因
self.onFulfilledCallbacks = []; // 存储成功的回调
self.onRejectedCallbacks = []; // 存储失败的回调
function resolve(value) {
if (self.status === "pending") {
self.status = "fulfilled";
self.value = value;
self.onFulfilledCallbacks.forEach(fn => fn());
}
}
function reject(reason) {
if (self.status === "pending") {
self.status = "rejected";
self.reason = reason;
self.onRejectedCallbacks.forEach(fn => fn());
}
}
try {
executor(resolve, reject); // 立即执行传入的函数
} catch (e) {
reject(e);
}
}
解释:
MyPromise构造函数接收一个执行器executor,该执行器是一个函数,立即被调用。self保存当前实例的引用,便于在内部函数中访问。status代表当前的状态,默认为"pending"(待定)。value和reason分别存储成功的结果和失败的原因。onFulfilledCallbacks和onRejectedCallbacks分别存储成功和失败的回调函数列表,以便在状态改变后调用它们。resolve函数用于将状态改为"fulfilled"并触发所有已注册的成功回调。reject函数用于将状态改为"rejected"并触发所有已注册的失败回调。- 如果执行器抛出异常,则直接调用
reject方法。
🧩 Step 2:实现 then 方法
MyPromise.prototype.then = function(onFulfilled, onRejected) {
let self = this;
if (self.status === "fulfilled") {
onFulfilled(self.value);
}
if (self.status === "rejected") {
onRejected(self.reason);
}
if (self.status === "pending") {
self.onFulfilledCallbacks.push(() => {
onFulfilled(self.value);
});
self.onRejectedCallbacks.push(() => {
onRejected(self.reason);
});
}
};
解释:
then方法接收两个参数:onFulfilled和onRejected,分别表示成功和失败的回调函数。- 如果当前状态是
"fulfilled",则立即执行onFulfilled回调,并传递成功的结果。 - 如果当前状态是
"rejected",则立即执行onRejected回调,并传递失败的原因。 - 如果当前状态是
"pending",则将onFulfilled和onRejected回调分别加入到相应的回调队列中,等到状态改变后再执行。
🎯 总结一下这段代码做了什么:
- 创建了一个对象
MyPromise,用来保存状态和值。 - 提供了
resolve和reject方法改变状态。 - 在
then中根据状态决定是否立即执行回调,或者先存起来等状态变化后再执行。
六、Promise 链式调用的原理(重要!)
Promise 最强大的功能之一就是链式调用:
new Promise((resolve) => {
setTimeout(() => resolve(1), 1000);
})
.then(res => {
console.log(res); // 1
return res * 2;
})
.then(res => {
console.log(res); // 2
return res * 2;
})
.then(res => {
console.log(res); // 4
});
🔍 原理说明:
- 每个
.then()返回一个新的 Promise。 - 如果你在
.then()中返回一个普通值,它会被自动包装成一个 resolved 的 Promise。 - 如果抛出异常,就会触发
.catch()。 - 如果返回的是另一个 Promise,它会等待这个 Promise 完成后再继续。
七、async/await 是 Promise 的语法糖
ES2017 引入了 async/await,让异步代码看起来像同步一样。
async function demo() {
const result1 = await new Promise(resolve => setTimeout(resolve, 1000, 'foo'));
console.log(result1); // foo
const result2 = await new Promise(resolve => setTimeout(resolve, 500, 'bar'));
console.log(result2); // bar
return 'done';
}
demo().then(console.log); // done
✅ async 函数的特点:
- 返回值一定是 Promise:无论你是否显式返回 Promise,async 函数总是返回一个 Promise。
- 可以使用
await关键字:只能在async函数中使用,用于等待一个 Promise 的结果,暂停函数的执行,直到 Promise 被解决。 - 错误处理可以用
try/catch:更接近同步写法,提高可读性和可维护性。
🔍 async/await 的本质:生成器 + Promise 自动执行器
虽然我们写的代码是 async/await,但它背后其实是基于 Generator(生成器)函数 和 Promise 的组合,再由 JS 引擎自动帮你执行的。
类比伪代码(帮助理解):
function* genDemo() {
const result1 = yield new Promise(resolve => setTimeout(resolve, 1000, 'foo'));
const result2 = yield new Promise(resolve => setTimeout(resolve, 500, 'bar'));
return 'done';
}
// 自动执行器
function run(gen) {
const iterator = gen();
function next(data) {
const result = iterator.next(data);
if (!result.done) {
result.value.then(next);
}
}
next();
}
run(genDemo).then(console.log); // done
可以看到,async/await 的本质其实就是这样一个自动生成并运行的 Generator 函数。
💡 await 的工作机制详解
const result = await someAsyncFunction(); // 假设返回一个 Promise
这行代码背后的逻辑是:
someAsyncFunction()返回一个 Promise。await会让整个async函数暂停执行,把控制权交还给事件循环。- 浏览器继续执行其他任务(如渲染、事件监听等)。
- 当这个 Promise 变为 fulfilled 状态后,JS 引擎会恢复执行这个
async函数,继续往下执行。 result变量将获得 Promise 的返回值。
⚠️ 注意:
await只能用在async函数中,否则会报错。
✅ 错误处理:使用 try/catch
async function demo() {
try {
const result = await fetchSomeData(); // 假设可能出错
console.log('获取数据成功:', result);
} catch (err) {
console.error('出错了:', err);
}
}
try/catch可以捕获await表达式中的错误。- 不再需要
.catch()来处理错误,使代码更加简洁直观。
✅ async 函数的返回值
无论你是否显式 return,async 函数都会返回一个 Promise:
async function test() {
return 'Hello'; // 实际上等于 Promise.resolve('Hello')
}
test().then(console.log); // Hello
如果你抛出异常:
async function test() {
throw new Error('出错了');
}
test().catch(console.error); // Error: 出错了
八、完整示例 + 详细解释
我们来写一个完整的流程,模拟登录 → 获取用户信息 → 获取订单数据。
✅ 步骤一:定义异步函数
function login(username, password) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (username === "admin" && password === "123456") {
resolve({ token: "abc123", userId: 123 });
} else {
reject("用户名或密码错误");
}
}, 1000);
});
}
function fetchUserInfo(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id: userId, name: "张三", age: 28 });
}, 1000);
});
}
function fetchOrders(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ orderId: 1, product: "iPhone", price: 999 },
{ orderId: 2, product: "MacBook", price: 1999 }
]);
}, 1000);
});
}
✅ 方式一:使用 Promise 链式调用(解决回调地狱)
相比前面提到的回调地狱,我们可以利用 Promise 来简化代码结构:
login("admin", "123456")
.then(tokenData => {
console.log("登录成功:", tokenData);
return fetchUserInfo(tokenData.userId);
})
.then(userInfo => {
console.log("用户信息:", userInfo);
return fetchOrders(userInfo.id);
})
.then(orders => {
console.log("订单列表:", orders);
})
.catch(err => {
console.error("错误:", err);
});
优势对比:
- 可读性增强:不再有层层嵌套,代码更加直观。
- 错误集中处理:只需一个
.catch()即可捕获所有错误,无需每层都处理。 - 易于扩展:可以轻松添加更多的异步操作,而不会使代码变得更加混乱。
✅ 方式二:使用 async/await(推荐)
async function run() {
try {
const tokenData = await login("admin", "123456");
console.log("登录成功:", tokenData);
const userInfo = await fetchUserInfo(tokenData.userId);
console.log("用户信息:", userInfo);
const orders = await fetchOrders(userInfo.id);
console.log("订单列表:", orders);
} catch (err) {
console.error("错误:", err);
}
}
run();
九、总结:Promise 的核心思想
| 特性 | 说明 |
|---|---|
| 状态管理 | pending → fulfilled/rejected,不可逆 |
| 回调缓存 | 在 pending 时缓存回调,等状态变化后执行 |
| 链式调用 | 每个 then 返回新 Promise,形成链条 |
| 错误传播 | 一个 reject 会沿着链找 catch 处理 |
| 微任务机制 | Promise.then 是微任务,优先于宏任务执行 |
| async/await | Promise 的语法糖,让异步更像同步 |
📌 记住一句话:Promise 不只是语法,更是思维方式。
通过本文的学习,希望能够帮助你摆脱回调地狱带来的困扰,拥抱更加清晰和高效的异步编程方式。