1. 从 "回调地狱" 到 Promise

在前端开发的异步编程领域,我们常常会遇到需要处理多个异步操作的情况。早期,使用回调函数来处理异步操作是一种常见的方式,但这种方式在处理复杂的异步流程时,会带来一个让人头疼的问题 ------ "回调地狱"。
1.1 回调地狱示例
假设我们有这样一个场景:李华向他的众多crush表白(人物纯属虚构,切勿带入个人)。使用传统的回调函数实现,代码可能会长这样:
javascript
function sendMessages(name, onFulfilled, onRejected) {
console.log(`李华 to ${name}:我喜欢你`);
console.log(`李华 等待 ${name}的回答`);
setTimeout(() => {
if (Math.random() <= 0.1) {
// 调用成功之后的回调
onFulfilled(`李华,爱老虎油`)
} else {
// 调用失败之后的回调
onRejected(`李华,抱歉,我有别人追了`)
}
}, 1000)
}
sendMessages(
"旺仔小乔",
(reply) => {
console.log("成功", reply)
},
(reply) => {
console.log("失败", reply);
sendMessages(
"可乐大乔",
(reply) => {
console.log("成功", reply);
},
(reply) => {
console.log("失败", reply);
sendMessages(
"伊利周瑜",
(reply) => {
console.log("成功", reply)
},
(reply) => {
console.log("失败", reply);
sendMessages(
"尖叫张飞",
(reply) => { "成功", reply },
(reply) => {
"失败", reply
console.log("李华,这辈子也就这样了");
}
)
}
)
}
)
}
)
这段代码虽然实现了我们的需求,但随着异步操作的增多,回调函数的嵌套会越来越深,代码会变得越来越难以阅读和维护。想象一下,如果这里有更多的文件需要读取,或者每个异步操作之间还有其他的逻辑,代码的复杂度将会呈指数级增长,这就是所谓的 "回调地狱"。
1.2 回调地狱的问题
- 代码可读性差:层层嵌套的回调函数使得代码的逻辑结构变得模糊,难以一眼看出各个异步操作之间的关系和执行顺序。
- 维护困难:当需要修改其中某个异步操作的逻辑时,可能需要在多层嵌套中找到对应的回调函数,并且还要小心不影响其他部分的代码。
- 错误处理复杂:每个回调函数都需要单独处理错误,这不仅增加了代码量,还容易出现错误处理不一致的情况。
1.3 Promise 应运而生
为了解决回调地狱的问题,ES6 引入了 Promise。Promise 是一个对象,它代表了一个异步操作的最终完成(或失败)及其结果值。简单来说,Promise 就是一个承诺,它承诺在未来的某个时间点会给你一个结果,这个结果可能是成功的数据,也可能是失败的原因。
使用 Promise 来改写上面读取文件的代码,会变得清晰很多:
javascript
sendMessages("旺仔小乔")
.then(
(reply) => {
console.log("成功", reply);
// 成功后返回一个已完成的Promise,终止链式调用
return Promise.resolve();
},
(reply) => {
console.log("失败", reply);
// 失败后返回新的Promise,继续下一个
return sendMessages("可乐大乔");
}
)
.then(
(reply) => {
console.log("成功", reply);
return Promise.resolve();
},
(reply) => {
console.log("失败", reply);
return sendMessages("伊利周瑜");
}
)
.then(
(reply) => {
console.log("成功", reply);
return Promise.resolve();
},
(reply) => {
console.log("失败", reply);
return sendMessages("尖叫张飞");
}
)
.then(
(reply) => {
console.log("成功", reply);
},
(reply) => {
console.log("失败", reply);
console.log("李华,这辈子也就这样了");
}
);
在这段代码中,我们使用了 Promise 的链式调用,每个 then 方法都返回一个新的 Promise,这样就避免了回调函数的层层嵌套,使得代码的逻辑更加清晰,错误处理也更加统一。
1.4 学习 Promise 的重要性
在现代前端开发中,Promise 已经成为了处理异步操作的核心机制之一。无论是使用原生的 JavaScript 进行开发,还是使用各种前端框架(如 Vue、React 等),都离不开 Promise。掌握 Promise 的使用,不仅可以让我们写出更优雅、更易维护的异步代码,还能更好地理解和运用其他与异步相关的技术,如 async/await 等。因此,深入学习 Promise 是每一位前端开发者都必不可少的功课。
接下来,让我们深入了解 Promise 的基本概念和用法,看看它是如何工作的,以及如何在实际项目中灵活运用它来解决各种异步编程问题。
2. Promise 初相识 (Promise A+规范)
2.1 概念与定义
Promise 是 ES6 引入的一种异步编程的新解决方案,它是一个对象,代表了一个异步操作的最终完成(或成功)及其结果值。简单来说,Promise 就像是一个容器,里面保存着某个未来才会结束的事件(通常是异步操作)的结果。从语法上讲,Promise 是一个对象,通过它可以获取异步操作的消息,并且提供了统一的 API,使得各种异步操作都能用同样的方式进行处理。

2.2 三个状态
Promise 有三种状态:
- Pending(等待态) :这是 Promise 的初始状态,此时异步操作尚未完成,既没有被兑现(成功)也没有被拒绝(失败)。
- Fulfilled(成功态) :当异步操作成功完成时,Promise 的状态会变为 Fulfilled,意味着操作已经成功,我们可以从 Promise 中获取到预期的结果。此时,Promise 会携带一个值,这个值就是异步操作成功的结果。
- Rejected(失败态) :如果异步操作失败或出现错误,Promise 的状态会变为 Rejected。此时,Promise 会携带一个原因,表示失败的原因。
状态转换规则如下:
- 一个 Promise 一旦从 Pending 状态变为 Fulfilled 或 Rejected,就不可以再进行其他状态转变,即 Promise 的状态只能改变一次。
- Promise 只能从 Pending 状态转换成 Fulfilled 或 Rejected 状态。具体转换流程为:当异步操作成功完成时,状态从 Pending 变为 Fulfilled,未来可以用 .then() 方法访问结果;当异步操作失败时,状态从 Pending 变为 Rejected,未来可以用 .catch() 方法访问错误信息 。
2.3 基本用法示例
下面通过一个简单的示例来展示 Promise 的基本使用:
javascript
// 创建一个 Promise
const myPromise = new Promise((resolve, reject) => {
// 模拟异步操作,这里使用 setTimeout
setTimeout(() => {
const success = true; // 模拟操作结果
if (success) {
resolve('操作成功'); // 将 Promise 状态置为 Fulfilled,并传递成功的值
} else {
reject('操作失败'); // 将 Promise 状态置为 Rejected,并传递失败的原因
}
}, 1000);
});
// 处理 Promise 的结果
myPromise.then((result) => {
console.log(result); // 输出: 操作成功
}).catch((error) => {
console.error(error); // 如果失败,捕获并输出错误
});
在上述代码中,我们首先使用 new Promise 创建了一个 Promise 对象,在其执行器函数中,通过 setTimeout 模拟了一个异步操作。如果 success 为 true,则调用 resolve 方法将 Promise 的状态变为 Fulfilled,并传递成功的结果 '操作成功';如果 success 为 false,则调用 reject 方法将 Promise 的状态变为 Rejected,并传递失败的原因 '操作失败'。
然后,我们使用 then 方法来处理 Promise 成功的情况,在 then 的回调函数中,我们可以获取到 resolve 传递过来的成功结果并进行相应的处理。使用 catch 方法来处理 Promise 失败的情况,在 catch 的回调函数中,我们可以获取到 reject 传递过来的失败原因并进行错误处理。
3. Promise 的核心方法
3.1 then 方法
then 方法是 Promise 中用于处理异步操作结果的核心方法之一,它允许我们在 Promise 状态变为 fulfilled 或 rejected 时执行相应的回调函数。
3.1.1 链式调用原理
then 方法的一个重要特性是它返回一个新的 Promise,这使得我们可以进行链式调用。具体来说,then 方法接收两个可选参数:onFulfilled 和 onRejected。
- onFulfilled:当 Promise 状态变为 fulfilled 时调用的回调函数,该函数接收 Promise 成功时的值作为参数。
- onRejected:当 Promise 状态变为 rejected 时调用的回调函数,该函数接收 Promise 失败时的原因作为参数。
当我们调用 then 方法时,它会返回一个新的 Promise,这个新 Promise 的状态和值取决于 then 方法中回调函数的执行结果:
- 如果 onFulfilled 或 onRejected 回调函数返回一个值,这个值会被包装成一个新的已解决(resolved)的 Promise,并作为 then 方法返回的新 Promise 的值。
- 如果 onFulfilled 或 onRejected 回调函数抛出一个错误,这个错误会被包装成一个新的已拒绝(rejected)的 Promise,并作为 then 方法返回的新 Promise 的值。
- 如果 onFulfilled 或 onRejected 回调函数返回一个 Promise,那么 then 方法返回的新 Promise 的状态和值将取决于这个返回的 Promise 的状态和值。
通过这种方式,我们可以将多个 then 方法串联起来,形成一个链式调用,每个 then 方法处理前一个 Promise 的结果,并返回一个新的 Promise 供下一个 then 方法处理。
3.1.2 示例与应用场景
假设我们有一个需求,需要先获取用户信息,然后根据用户信息获取用户的订单列表,最后统计订单的总金额。使用 then 方法的链式调用可以很方便地实现这个需求:
javascript
// 模拟获取用户信息的异步操作
function getUserInfo() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const user = { id: 1, name: 'John' };
resolve(user);
}, 1000);
});
}
// 模拟根据用户信息获取订单列表的异步操作
function getOrderList(user) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const orders = [
{ id: 1, amount: 100, userId: user.id },
{ id: 2, amount: 200, userId: user.id }
];
resolve(orders);
}, 1000);
});
}
// 统计订单总金额
function calculateTotalAmount(orders) {
return orders.reduce((total, order) => total + order.amount, 0);
}
getUserInfo()
.then(user => getOrderList(user))
.then(orders => calculateTotalAmount(orders))
.then(totalAmount => console.log(`订单总金额为: ${totalAmount}`))
.catch(error => console.error('操作过程中出现错误:', error));
在这个示例中,getUserInfo 返回一个 Promise,当这个 Promise 成功时,then 方法会调用 getOrderList 并传入用户信息,getOrderList 又返回一个 Promise,当这个 Promise 成功时,下一个 then 方法会调用 calculateTotalAmount 并传入订单列表,最后计算出订单总金额并打印。如果在任何一个步骤中出现错误,catch 方法会捕获并处理错误。
3.2 catch 方法
catch 方法是 Promise 中用于捕获错误的方法,它是 then 方法的语法糖,用于简化错误处理。
3.2.1 错误捕获机制
catch 方法实际上是 then(null, onRejected) 的简写形式,它专门用于捕获 Promise 链中任何一个环节抛出的错误。当 Promise 链中的某个 Promise 被拒绝(rejected)时,如果之前的 then 方法没有提供 onRejected 回调函数来处理错误,那么这个错误会一直向后传递,直到被 catch 方法捕获。
例如:
javascript
new Promise((resolve, reject) => {
setTimeout(() => {
reject('操作失败');
}, 1000);
})
.then(result => console.log(result))
.catch(error => console.error('捕获到错误:', error));
在这个例子中,Promise 被拒绝并传递了错误信息 '操作失败',由于第一个 then 方法没有提供 onRejected 回调函数,所以错误会被后面的 catch 方法捕获并处理。
3.2.2 错误处理最佳实践
在项目中使用 catch 方法进行错误处理时,有一些最佳实践建议:
- 统一错误处理:在 Promise 链的末尾使用 catch 方法来捕获所有可能的错误,这样可以确保错误不会被遗漏,并且可以在一个地方统一处理错误。
- 错误日志记录:在 catch 方法中,除了对错误进行处理外,还应该记录错误日志,以便后续排查问题。可以使用浏览器的控制台日志或者专业的日志记录工具。
- 避免在 catch 中抛出新错误:尽量避免在 catch 方法中再次抛出新的错误,因为这会导致错误处理变得更加复杂,并且可能会使错误在 Promise 链中继续传递,难以追踪。如果确实需要在 catch 中处理错误后返回一个新的 Promise,应该使用 resolve 或 reject 来处理,而不是抛出新错误。
3.3 finally 方法
finally 方法是 ES9(ES2018) 引入的 Promise 方法,它的特点是无论 Promise 的状态是 fulfilled 还是 rejected,都会执行其中的回调函数。
finally 方法主要用于一些资源清理的场景,比如在异步操作完成后关闭文件、释放网络连接等。它的回调函数不接收任何参数,因为它不关心 Promise 的最终状态是成功还是失败。
例如,我们在使用 fetch 进行网络请求时,可以使用 finally 方法来显示加载状态的结束:
javascript
function fetchData() {
console.log('开始加载数据...');
return fetch('https://api.example.com/data')
.then(response => response.json())
.catch(error => console.error('请求出错:', error))
.finally(() => console.log('数据加载结束'));
}
fetchData();
在这个例子中,无论 fetch 请求成功还是失败,finally 方法中的回调函数都会执行,打印出 '数据加载结束',这样可以确保加载状态的显示与实际的异步操作完成情况一致。
4. Promise 的高级应用
4.1 Promise.all
4.1.1 并发请求处理
Promise.all 方法用于并行处理多个异步任务,它接收一个包含多个 Promise 对象的可迭代对象(如数组)作为参数,并返回一个新的 Promise。只有当传入的所有 Promise 都成功完成(状态变为 fulfilled)时,返回的新 Promise 才会成功,其结果是一个包含所有成功结果的数组,且数组中结果的顺序与传入的 Promise 顺序一致。如果其中任何一个 Promise 失败(状态变为 rejected),则返回的新 Promise 会立即失败,失败原因是第一个失败的 Promise 的错误信息。
例如,我们有三个异步任务,分别模拟从不同的 API 获取数据:
javascript
function fetchData1() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('数据1');
}, 1000);
});
}
function fetchData2() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('数据2');
}, 2000);
});
}
function fetchData3() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('数据3');
}, 1500);
});
}
Promise.all([fetchData1(), fetchData2(), fetchData3()])
.then(results => {
console.log(results); // 输出: ['数据1', '数据2', '数据3']
})
.catch(error => {
console.error(error);
});
在这个例子中,Promise.all 会同时启动这三个异步任务,尽管它们的完成时间不同,但最终会等待所有任务都完成后,将结果以数组的形式返回。如果其中某个任务失败,比如 fetchData2 改为:
javascript
function fetchData2() {
return new Promise((_, reject) => {
setTimeout(() => {
reject('获取数据2失败');
}, 2000);
});
}
那么 Promise.all 返回的 Promise 会立即失败,catch 方法会捕获到错误信息 '获取数据2失败'。
4.1.2 实际项目案例
在一个电商项目中,我们可能需要在商品详情页面展示商品的基本信息、评论列表和相关推荐商品。这些数据分别来自不同的 API 接口,我们可以使用 Promise.all 来并发请求这些数据,从而提高页面的加载效率。
javascript
// 获取商品基本信息
function getProductInfo(productId) {
return fetch(`https://api.example.com/products/${productId}`)
.then(response => response.json());
}
// 获取商品评论列表
function getProductReviews(productId) {
return fetch(`https://api.example.com/products/${productId}/reviews`)
.then(response => response.json());
}
// 获取相关推荐商品
function getRelatedProducts(productId) {
return fetch(`https://api.example.com/products/${productId}/related`)
.then(response => response.json());
}
const productId = 123;
Promise.all([
getProductInfo(productId),
getProductReviews(productId),
getRelatedProducts(productId)
])
.then(([productInfo, reviews, relatedProducts]) => {
// 处理数据,展示在页面上
console.log('商品基本信息:', productInfo);
console.log('商品评论列表:', reviews);
console.log('相关推荐商品:', relatedProducts);
})
.catch(error => {
console.error('请求数据失败:', error);
});
通过这种方式,我们可以并行地获取这三个数据,而不需要依次等待每个请求完成,大大缩短了页面的加载时间,提升了用户体验。如果其中任何一个请求失败,整个 Promise.all 就会失败,我们可以在 catch 方法中统一处理错误。
4.2 Promise.race
Promise.race 方法同样接收一个包含多个 Promise 对象的可迭代对象作为参数,并返回一个新的 Promise。它的特点是,只要传入的 Promise 中有一个率先完成(无论是成功还是失败),返回的新 Promise 就会立即以这个率先完成的 Promise 的结果或错误进行解决或拒绝。
例如,我们有两个异步任务,一个任务模拟成功响应,另一个任务模拟失败响应:
javascript
function task1() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('任务1成功');
}, 2000);
});
}
function task2() {
return new Promise((_, reject) => {
setTimeout(() => {
reject('任务2失败');
}, 1000);
});
}
Promise.race([task1(), task2()])
.then(result => {
console.log(result); // 不会执行
})
.catch(error => {
console.error(error); // 输出: 任务2失败
});
在这个例子中,task2 会在 1 秒后失败,而 task1 需要 2 秒后才成功,所以 Promise.race 返回的 Promise 会立即失败,错误信息为 '任务2失败'。
Promise.race 常用于设置请求超时的场景。比如,我们发起一个网络请求获取数据,但希望在一定时间内如果没有收到响应,就视为请求超时并进行相应处理:
javascript
function fetchData() {
return fetch('https://api.example.com/data')
.then(response => response.json());
}
function timeout(duration) {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('请求超时'));
}, duration);
});
}
Promise.race([fetchData(), timeout(3000)])
.then(data => {
console.log('请求成功:', data);
})
.catch(error => {
console.error('请求失败:', error.message);
});
在这个例子中,fetchData 发起网络请求获取数据,timeout(3000) 创建一个 3 秒后失败的 Promise。Promise.race 会同时执行这两个 Promise,如果 fetchData 在 3 秒内成功获取数据,就会执行 then 方法处理数据;如果 3 秒内 fetchData 没有完成,timeout 会率先失败,Promise.race 返回的 Promise 就会失败,执行 catch 方法,提示请求超时。
4.3 Promise.allSettled
Promise.allSettled 方法也接收一个包含多个 Promise 对象的可迭代对象作为参数,并返回一个新的 Promise。与 Promise.all 不同的是,Promise.allSettled 会等待所有传入的 Promise 都有结果(无论成功还是失败),然后返回一个包含所有 Promise 状态和结果的数组。数组中的每个元素是一个对象,包含 status(表示 Promise 的状态,取值为 'fulfilled' 或 'rejected')和 value(如果状态是 fulfilled,则为成功的结果;如果状态是 rejected,则为失败的原因)。
例如:
javascript
function task1() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('任务1成功');
}, 1000);
});
}
function task2() {
return new Promise((_, reject) => {
setTimeout(() => {
reject('任务2失败');
}, 2000);
});
}
Promise.allSettled([task1(), task2()])
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`任务${index + 1}成功,结果为:`, result.value);
} else {
console.log(`任务${index + 1}失败,原因是:`, result.reason);
}
});
});
在这个例子中,Promise.allSettled 会等待 task1 和 task2 都有结果后,返回一个包含两个对象的数组。第一个对象表示 task1 的状态和结果,第二个对象表示 task2 的状态和原因。通过这种方式,我们可以全面了解每个异步任务的执行情况,而不会因为某个任务的失败而中断对其他任务结果的处理。
Promise.allSettled 适用于一些需要对多个异步操作的结果进行汇总分析的场景。比如在一个批量数据处理的任务中,我们需要向多个服务器发送数据更新请求,无论每个请求是否成功,都需要记录下每个请求的结果,以便后续进行错误排查和统计分析:
javascript
function updateServer1(data) {
return new Promise((resolve, reject) => {
// 模拟网络请求
setTimeout(() => {
if (Math.random() > 0.5) {
resolve('服务器1更新成功');
} else {
reject('服务器1更新失败');
}
}, 1000);
});
}
function updateServer2(data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
resolve('服务器2更新成功');
} else {
reject('服务器2更新失败');
}
}, 1500);
});
}
const data = { key: 'value' };
Promise.allSettled([updateServer1(data), updateServer2(data)])
.then(results => {
let successCount = 0;
let failureCount = 0;
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
successCount++;
console.log(`服务器${index + 1}更新成功,结果为:`, result.value);
} else {
failureCount++;
console.log(`服务器${index + 1}更新失败,原因是:`, result.reason);
}
});
console.log(`成功更新的服务器数量: ${successCount}`);
console.log(`更新失败的服务器数量: ${failureCount}`);
});
在这个例子中,Promise.allSettled 可以帮助我们统计出成功和失败的服务器更新次数,方便对整个批量更新操作的结果进行评估和后续处理。
4.4 Promise.any
Promise.any 方法接收一个包含多个 Promise 对象的可迭代对象作为参数,并返回一个新的 Promise。它的特性是只要传入的 Promise 中有一个成功(状态变为 fulfilled),返回的新 Promise 就会立即成功,其结果就是第一个成功的 Promise 的结果。如果所有传入的 Promise 都失败(状态变为 rejected),则返回的新 Promise 会失败,并抛出一个 AggregateError 错误,该错误包含所有失败的原因。
例如:
javascript
function task1() {
return new Promise((_, reject) => {
setTimeout(() => {
reject('任务1失败');
}, 2000);
});
}
function task2() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('任务2成功');
}, 1000);
});
}
function task3() {
return new Promise((_, reject) => {
setTimeout(() => {
reject('任务3失败');
}, 1500);
});
}
Promise.any([task1(), task2(), task3()])
.then(result => {
console.log('第一个成功的任务结果:', result); // 输出: 任务2成功
})
.catch(error => {
console.error('所有任务都失败:', error);
});
在这个例子中,task2 会在 1 秒后成功,虽然 task1 和 task3 会失败,但 Promise.any 只关注第一个成功的 Promise,所以会立即返回 task2 的成功结果。
Promise.any 适用于需要获取第一个成功结果的场景。比如在一个应用中,我们有多个数据源可以获取用户的偏好设置,但只要从其中一个数据源成功获取到数据,就可以满足需求,不需要等待其他数据源的响应:
javascript
function fetchPreferencesFromSource1() {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
resolve({ theme: 'dark' });
} else {
reject('数据源1获取失败');
}
}, 1000);
});
}
function fetchPreferencesFromSource2() {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
resolve({ language: 'en' });
} else {
reject('数据源2获取失败');
}
}, 1500);
});
}
function fetchPreferencesFromSource3() {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
resolve({ notifications: true });
} else {
reject('数据源3获取失败');
}
}, 2000);
});
}
Promise.any([fetchPreferencesFromSource1(), fetchPreferencesFromSource2(), fetchPreferencesFromSource3()])
.then(preferences => {
console.log('获取到用户偏好设置:', preferences);
})
.catch(error => {
console.error('所有数据源获取失败:', error);
});
在这个例子中,只要有一个数据源成功获取到用户偏好设置,Promise.any 就会返回该结果,从而快速满足应用对用户偏好数据的需求。如果所有数据源都获取失败,catch 方法会捕获到 AggregateError 错误,我们可以根据具体情况进行处理,比如提示用户获取偏好设置失败。
5. Promise 与 async/await
5.1 async/await 语法糖
async/await 是 ES2017 引入的异步编程语法糖,它建立在 Promise 的基础之上,使得异步代码看起来更像是同步代码,极大地提升了代码的可读性和可维护性 。async 用于声明一个异步函数,该函数始终返回一个 Promise 对象。await 关键字只能在 async 函数内部使用,用于等待一个 Promise 对象的解决(resolved)或拒绝(rejected),它会暂停当前 async 函数的执行,直到被等待的 Promise 对象返回结果。
例如:
javascript
async function asyncFunction() {
try {
const result = await new Promise((resolve) => {
setTimeout(() => {
resolve('异步操作结果');
}, 1000);
});
console.log(result); // 输出: 异步操作结果
} catch (error) {
console.error(error);
}
}
asyncFunction();
在这个例子中,asyncFunction 是一个异步函数,await 等待 new Promise 返回的结果,当 Promise 成功解决后,await 会将结果赋值给 result,然后继续执行后面的代码。如果 Promise 被拒绝,catch 块会捕获到错误并进行处理。
5.2 对比与优势
- 写法对比:使用 Promise 时,我们通过链式调用 then 方法来处理异步操作的结果,而 async/await 则通过 await 关键字来等待 Promise 的结果,使代码看起来更像同步代码。例如,同样是获取用户信息并根据用户信息获取订单列表的操作:
-
- Promise 写法:
javascript
function getUserInfo() {
return new Promise((resolve) => {
setTimeout(() => {
const user = { id: 1, name: 'John' };
resolve(user);
}, 1000);
});
}
function getOrderList(user) {
return new Promise((resolve) => {
setTimeout(() => {
const orders = [
{ id: 1, amount: 100, userId: user.id },
{ id: 2, amount: 200, userId: user.id }
];
resolve(orders);
}, 1000);
});
}
getUserInfo()
.then(user => getOrderList(user))
.then(orders => console.log(orders))
.catch(error => console.error(error));
- async/await 写法:
javascript
async function getUserAndOrders() {
try {
const user = await getUserInfo();
const orders = await getOrderList(user);
console.log(orders);
} catch (error) {
console.error(error);
}
}
getUserAndOrders();
可以看出,async/await 的写法更加简洁直观,代码结构更接近同步代码,阅读起来更容易理解。
- 错误处理优势:在 Promise 中,错误处理通常通过 catch 方法在链式调用的末尾进行捕获。而在 async/await 中,可以使用传统的 try...catch 语句来捕获错误,这种方式更加符合我们处理同步代码错误的习惯,使得错误处理的代码位置更清晰,也更容易维护。例如,在上面的代码中,如果 getUserInfo 或 getOrderList 抛出错误,在 Promise 写法中,需要在最后一个 then 方法之后的 catch 中处理;而在 async/await 写法中,可以直接在 try 块中捕获并在 catch 块中处理,更方便定位和处理错误。
5.3 实际应用示例
在实际项目中,async/await 常用于处理网络请求、数据库操作等异步任务。以一个简单的前端项目为例,我们使用 fetch 进行网络请求获取数据,并使用 async/await 进行处理:
xml
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>async/await 示例</title>
</head>
<body>
<script>
async function fetchData() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
if (!response.ok) {
throw new Error('网络请求失败');
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error('获取数据失败:', error);
}
}
fetchData();
</script>
</body>
</html>
在这段代码中,fetchData 是一个异步函数,使用 await 等待 fetch 请求返回的 Promise 对象,然后检查响应状态,如果状态正常,再等待解析 JSON 数据。如果在任何一步出现错误,catch 块会捕获并处理错误。这种写法使得网络请求的处理逻辑更加清晰,易于理解和维护。通过这个示例,我们可以看到 async/await 在实际项目中的应用场景和优势,它让异步编程变得更加简单和高效。
6. Promise 的原理与实现
6.1 内部机制剖析
Promise 的核心是一个状态机,它的内部机制涉及到状态管理、任务队列以及微任务与宏任务的处理。
- 状态管理:Promise 有三种状态:Pending(等待态)、Fulfilled(成功态)和Rejected(失败态)。状态的转换是单向且不可逆的,一旦从Pending状态转变为Fulfilled或Rejected状态,就不能再发生改变。当我们创建一个 Promise 时,它会初始化为Pending状态。在异步操作完成后,根据结果调用resolve函数将状态转变为Fulfilled,并传递成功的值;或者调用reject函数将状态转变为Rejected,并传递失败的原因。
- 任务队列:Promise 内部维护了一个任务队列,用于存储在then方法中注册的回调函数。当 Promise 的状态发生改变时,会触发任务队列中的回调函数执行。例如,当状态变为Fulfilled时,会依次执行任务队列中onFulfilled回调函数;当状态变为Rejected时,会依次执行onRejected回调函数 。
- 微任务与宏任务:在 JavaScript 的事件循环机制中,任务分为宏任务(macrotask)和微任务(microtask)。宏任务如setTimeout、setInterval、script(整体代码)等,微任务如Promise.then、MutationObserver等。Promise 的then方法注册的回调函数属于微任务。当一个宏任务执行完毕后,会先检查微任务队列,并依次执行微任务队列中的所有任务,然后再去执行下一个宏任务。这就保证了 Promise 的then回调函数总是在当前宏任务结束后,下一个宏任务开始前执行。例如:
javascript
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve()
.then(() => {
console.log('promise1');
})
.then(() => {
console.log('promise2');
});
console.log('script end');
在这段代码中,首先输出script start和script end,这是同步代码的执行。然后setTimeout被放入宏任务队列,Promise.resolve().then被放入微任务队列。由于微任务优先于宏任务执行,所以先输出promise1和promise2,最后输出setTimeout。通过这样的机制,Promise 实现了异步操作的有序处理和回调函数的延迟执行,使得异步编程更加可控和可预测。
6.2 手写 Promise 简易版
为了更深入地理解 Promise 的工作原理,我们可以手写一个简易版的 Promise。下面是一个基本的实现,它包含了 Promise 的核心功能:状态管理、then方法的链式调用以及错误处理。
ini
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED ='rejected';
class MyPromise {
constructor(executor) {
this.state = PENDING;
this.value = undefined;
this.reason = undefined;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.state === PENDING) {
this.state = FULFILLED;
this.value = value;
this.onFulfilledCallbacks.forEach(callback => callback(this.value));
}
};
const reject = (reason) => {
if (this.state === PENDING) {
this.state = REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach(callback => callback(this.reason));
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function'? onFulfilled : value => value;
onRejected = typeof onRejected === 'function'? onRejected : reason => { throw reason };
return new MyPromise((nextResolve, nextReject) => {
if (this.state === FULFILLED) {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
this.#resolvePromise(nextResolve, nextReject, x);
} catch (error) {
nextReject(error);
}
}, 0);
}
if (this.state === REJECTED) {
setTimeout(() => {
try {
const x = onRejected(this.reason);
this.#resolvePromise(nextResolve, nextReject, x);
} catch (error) {
nextReject(error);
}
}, 0);
}
if (this.state === PENDING) {
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
this.#resolvePromise(nextResolve, nextReject, x);
} catch (error) {
nextReject(error);
}
}, 0);
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
this.#resolvePromise(nextResolve, nextReject, x);
} catch (error) {
nextReject(error);
}
}, 0);
});
}
});
}
catch(onRejected) {
return this.then(null, onRejected);
}
#resolvePromise(nextResolve, nextReject, x) {
if (x === this) {
return nextReject(new TypeError('Chaining cycle detected for promise'));
}
if (x instanceof MyPromise) {
x.then(nextResolve, nextReject);
} else if (x!== null && (typeof x === 'object' || typeof x === 'function')) {
let called = false;
try {
const then = x.then;
if (typeof then === 'function') {
then.call(x, (y) => {
if (called) return;
called = true;
this.#resolvePromise(nextResolve, nextReject, y);
}, (r) => {
if (called) return;
called = true;
nextReject(r);
});
} else {
nextResolve(x);
}
} catch (error) {
if (called) return;
called = true;
nextReject(error);
}
} else {
nextResolve(x);
}
}
}
在这个实现中:
- constructor方法接收一个执行器函数executor,并初始化 Promise 的状态为PENDING,同时定义了用于存储成功值value、失败原因reason以及成功和失败回调函数的数组onFulfilledCallbacks和onRejectedCallbacks。在执行器函数中,调用resolve和reject函数来改变 Promise 的状态,并触发相应的回调函数。
- then方法用于处理 Promise 的成功和失败情况。它接收两个回调函数onFulfilled和onRejected,并返回一个新的 Promise。在then方法中,根据当前 Promise 的状态,决定是立即执行回调函数,还是将回调函数添加到相应的回调函数数组中等待状态改变时执行。如果回调函数返回一个值,会根据这个值的类型来决定如何处理新的 Promise;如果返回的是一个 Promise,则等待这个 Promise 完成后再决定新 Promise 的状态。
- catch方法是then(null, onRejected)的简写,用于捕获 Promise 链中的错误。
- resolvePromise方法是一个内部方法,用于处理then方法中回调函数返回值为 Promise 或者其他对象的情况,确保正确地处理链式调用和状态传递。通过这个简易版的 Promise 实现,我们可以更清晰地看到 Promise 的工作原理,包括状态的管理、回调函数的执行以及链式调用的实现机制,有助于我们在实际开发中更好地理解和运用 Promise。
随着前端技术的不断发展,异步编程将继续在前端开发中扮演重要的角色。希望透过这篇文章让大家理解Promise,Promise 作为异步编程的基础,也将不断演进和完善。希望大家在今后的前端开发中,能够熟练运用 Promise,让我们的代码更加优雅、高效!如果大家在学习和使用 Promise 的过程中有任何问题或心得,欢迎在评论区留言交流,也可以前往我的github 网站查看更多关于 Promise 的内容。
如果您觉得这篇文章对您有帮助,欢迎点赞和收藏,大家的支持是我继续创作优质内容的动力🌹🌹🌹也希望您能在😉😉😉我的主页 😉😉😉找到更多对您有帮助的内容。
- 致敬每一位赶路人