JavaScript 异步编程:Promise 的使用与反模式
1. 异步函数中的错误处理
在 JavaScript 异步编程中,使用
try-catch
块处理
await
关键字等待的 Promise 非常重要。如果
try
块中等待的 Promise 被拒绝,
await
表达式之后的代码将不会执行,执行流程会跳转到
catch
块。
await
关键字会抛出 Promise 的拒绝值,使
catch
块能够捕获该拒绝。
例如:
async function getUsersAndTasks() {
try {
const users = await fetchUsers();
const tasks = await fetchTasks();
// 对 users 和 tasks 进行操作
} catch (error) {
// 处理错误
}
}
另外,也可以省略
try-catch
块,但调用异步函数的代码必须处理 Promise 的拒绝,可以使用 Promise 链式调用:
async function getUsersAndTasks() {
const users = await fetchUsers();
const tasks = await fetchTasks();
// 对 users 和 tasks 进行操作
}
getUsersAndTasks().catch((error) => {
/* 处理错误 */
});
或者在调用代码中使用
try-catch
块(如果使用
async-await
语法):
async function getUsersAndTasks() {
const users = await fetchUsers();
const tasks = await fetchTasks();
// 对 users 和 tasks 进行操作
}
async function initApp() {
try {
await getUsersAndTasks();
} catch (error) {
// 处理错误
}
}
2. 返回 Promise 与等待 Promise
在异步函数中忘记等待 Promise 可能会导致错误和意外输出。例如:
// 返回一个可能成功或失败的 Promise
function getPromise() {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() < 0.5) {
resolve("success");
} else {
reject(new Error("failed"));
}
}, 1000);
});
}
async function foo() {
getPromise();
}
foo()
.then(() => console.log("foo promise fulfilled"))
.catch(() => console.log("foo promise rejected"));
上述代码中,
getPromise
函数返回的 Promise 可能被拒绝,但
foo
函数返回的 Promise 总是被解决。原因是
foo
函数没有返回或等待
getPromise
函数返回的 Promise,它只是调用了
getPromise
函数,函数执行结束并隐式返回
undefined
,导致
foo
函数的 Promise 以
undefined
作为解决值被解决。
为了捕获
getPromise
函数返回的 Promise 的拒绝,可以采取以下方法:
-
返回
getPromise
函数返回的 Promise
:
async function foo() {
return getPromise();
}
foo()
.then(() => console.log("foo promise fulfilled"))
.catch(() => console.log("foo promise failed"));
返回 Promise 后,
foo
函数的 Promise 会与
getPromise
函数返回的 Promise 有相同的结果。
-
等待
getPromise
函数返回的 Promise
:
async function foo() {
await getPromise();
}
foo()
.then(() => console.log("foo promise fulfilled"))
.catch(() => console.log("foo promise failed"));
如果等待的 Promise 被拒绝,其拒绝值会在异步函数内部抛出。由于
foo
函数内部没有
try-catch
块,Promise 的拒绝会导致
foo
函数的 Promise 也以相同的拒绝原因被拒绝。需要注意的是,如果
getPromise
函数返回的 Promise 被解决,
foo
函数的 Promise 会以
undefined
作为解决值,因为没有从
foo
函数中显式返回任何值。
-
等待
getPromise
函数返回的 Promise 并使用
try-catch
块包围
:
async function foo() {
try {
await getPromise();
} catch (error) {
console.log("inside catch block of foo function");
return "error caught in foo";
}
}
foo()
.then(() => console.log("foo promise fulfilled"))
.catch(() => console.log("foo promise failed"));
等待
getPromise
调用会捕获 Promise 的拒绝,使
catch
块执行。但
foo
函数返回的 Promise 总是被解决,因为
catch
块没有抛出错误或返回被拒绝的 Promise。如果只在
catch
块中抛出错误,最好省略
try-catch
块,让 Promise 的拒绝自动拒绝
foo
函数的 Promise。此外,如果 Promise 被解决且
catch
块未执行,没有显式返回任何值,
foo
函数的 Promise 会以
undefined
作为解决值。在
await
表达式前添加
return
关键字可以解决这个问题。
3. 等待非 Promise 值
await
关键字通常用于等待 Promise 解决,但也可以用于非 Promise 值。例如:
const printRandomNumber = async () => {
const randomNum = await Math.floor(Math.random() * 10);
console.log(randomNum);
};
printRandomNumber();
console.log("before printing random number");
执行上述代码时,会发现最后一行的
console.log
语句在随机数打印之前被记录,尽管函数调用在最后一个
console.log
语句之前。这是因为当
await
关键字与非 Promise 值一起使用时,会创建一个新的 Promise,并以该值作为解决值。
await
表达式之后的代码会像在解决处理程序中一样异步执行,只有在同步代码执行结束后才会执行异步代码。
4. 微任务和任务
执行 DOM 事件监听器、
setTimeout
或
setInterval
回调需要调度“任务”,任务会被放入任务队列,直到事件循环处理它们。而 Promise 的解决或拒绝处理程序的执行需要调度“微任务”。微任务(ECMAScript 规范称为作业)的优先级高于“任务”,会在以下情况后处理:
- 每个回调之后(前提是调用栈为空)。
- 每个任务之后。
“任务”按其在任务队列中的入队顺序执行,事件循环每一轮只执行一个任务。而微任务会一直处理,直到微任务队列为空。如果一个微任务调度了另一个微任务,该微任务也会被处理,这可能导致事件循环陷入无限循环。
例如:
console.log("start");
setTimeout(() => {
console.log("setTimeout callback with 500ms delay");
}, 500);
Promise.resolve()
.then(() => {
console.log("first 'then' callback");
})
.then(() => {
console.log("second 'then' callback");
})
.then(() => {
console.log("third 'then' callback");
});
setTimeout(() => {
console.log("setTimeout callback with 0ms delay");
}, 0);
console.log("end");
执行上述代码的步骤如下:
1. 创建一个任务来执行脚本,开始同步代码的执行。
2. 执行第一个
console.log
语句,在控制台记录 “start”。
3. 调用
setTimeout
函数,延迟 500ms,会在后台启动一个定时器,定时器到期后会将执行
setTimeout
回调的任务放入任务队列。
4. 调用
Promise.resolve
创建一个已解决的 Promise,为执行其解决处理程序,将一个微任务放入微任务队列。
5. 调用
setTimeout
函数,延迟 0ms,调度一个任务来执行其回调。
6. 同步执行到达最后一个
console.log
语句,记录 “end”。此时调用栈为空,事件循环可以开始处理调度的任务和微任务。
7. 由于微任务在每个任务和每个回调之后(调用栈为空时)处理,同步代码执行结束后,事件循环会处理微任务队列中的微任务,记录 “first ‘then’ callback”。
8. 第一个
then
方法的回调函数隐式返回
undefined
,导致该
then
方法返回的 Promise 以
undefined
作为解决值,将另一个微任务放入微任务队列以执行该 Promise 的解决处理程序。
9. 微任务会一直处理,直到微任务队列为空,记录 “second ‘then’ callback”。
10. 第二个
then
方法的回调函数隐式返回
undefined
,将另一个微任务放入微任务队列以执行该 Promise 的解决处理程序。
11. 记录 “third ‘then’ callback”。
12. 忽略第三个
then
方法返回的 Promise 的解决,所有微任务处理完毕,微任务队列为空,事件循环开始处理任务队列中的第一个任务。
13. 任务队列中的第一个任务是第二个
setTimeout
调用的任务,处理该任务会记录 “setTimeout callback with 0ms delay”。
14. 最后处理第一个
setTimeout
调用的任务,记录 “setTimeout callback with 500ms delay”。
5. Promise 相关的反模式
在 JavaScript 异步编程中,有一些常见的 Promise 相关反模式需要避免:
-
不必要地使用 Promise 构造函数
:
许多 JavaScript 开发者,尤其是对 Promise 经验不足的开发者,会不必要地使用
Promise
构造函数创建 Promise。例如:
function fetchData(url) {
return new Promise((resolve, reject) => {
fetch(url)
.then((res) => res.json(res))
.then(resolve)
.catch(reject);
});
}
上述代码可以正常工作,但使用
Promise
构造函数是不必要的,因为
fetch
函数已经返回一个 Promise。可以重写为:
function fetchData(url) {
return fetch(url).then((res) => res.json(res));
}
重写后的
fetchData
函数更简洁、易读,避免了创建不必要的 Promise,并且调用该函数的代码可以捕获和处理任何错误。不必要地使用 Promise 构造函数还可能导致问题,如果忘记在 Promise 链中添加
catch
方法,HTTP 请求期间抛出的任何错误都不会被捕获。
-
错误处理不当
:
编写使用 Promise 的代码时,重要的规则是要么捕获并处理错误,要么返回 Promise 让调用代码捕获和处理。例如:
function fetchData(url) {
fetch(url).then((response) => response.json());
}
fetchData("https://jsonplaceholder.typicode.com/todos/1")
.then((data) => console.log(data))
.catch((error) => console.log(error));
上述代码会抛出错误,因为
fetchData
函数没有返回 Promise,也不允许调用代码进行任何错误处理。可以通过在
fetch(...)
前添加
return
关键字来修复:
function fetchData(url) {
return fetch(url).then((response) => response.json());
}
这样调用代码就可以负责使用响应数据和处理任何错误。
总结
在 JavaScript 异步编程中,正确处理 Promise 及其错误是非常重要的。避免不必要地使用 Promise 构造函数,确保正确处理错误,合理使用
await
关键字和微任务、任务的调度,能够编写出更健壮、高效的异步代码。
JavaScript 异步编程:Promise 的使用与反模式
6. 反模式之将 Promise 拒绝转换为解决
有时候开发者会不小心将 Promise 的拒绝转换为解决,这会隐藏潜在的错误,使调试变得困难。例如:
function getData() {
return new Promise((resolve, reject) => {
// 模拟一个可能失败的异步操作
if (Math.random() < 0.5) {
reject(new Error('Operation failed'));
} else {
resolve('Success');
}
})
.catch(() => {
// 错误处理中直接 resolve,将拒绝转换为解决
resolve('Fallback value');
});
}
getData()
.then((data) => console.log(data))
.catch((error) => console.log(error));
在上述代码中,
getData
函数中的
catch
块捕获到错误后,直接调用
resolve
将 Promise 转换为解决状态。这会导致调用者无法得知操作实际上失败了。正确的做法是在
catch
块中进行必要的处理后,重新抛出错误或者返回一个被拒绝的 Promise,让调用者能够正确处理错误。
7. 反模式之异步执行器函数
使用异步函数作为
Promise
构造函数的执行器函数也是一种反模式。例如:
function asyncExecutor() {
return new Promise(async (resolve, reject) => {
try {
const result = await someAsyncOperation();
resolve(result);
} catch (error) {
reject(error);
}
});
}
这里使用了异步函数作为执行器,虽然代码可以正常工作,但这是不必要的。
Promise
构造函数的执行器是同步执行的,使用异步函数会增加不必要的复杂性。可以直接返回异步操作的 Promise,而不需要使用
Promise
构造函数来包装。例如:
function asyncExecutor() {
return someAsyncOperation();
}
8. 总结与最佳实践
为了避免上述提到的反模式,在编写 JavaScript 异步代码时,可以遵循以下最佳实践:
| 反模式 | 避免方法 |
| ---- | ---- |
| 不必要地使用 Promise 构造函数 | 优先使用已有的异步函数返回的 Promise,避免手动包装 |
| 错误处理不当 | 确保每个 Promise 链都有合适的错误处理,要么在当前函数中处理,要么返回 Promise 让调用者处理 |
| 将 Promise 拒绝转换为解决 | 在
catch
块中进行必要处理后,重新抛出错误或返回被拒绝的 Promise |
| 异步执行器函数 | 直接返回异步操作的 Promise,不使用
Promise
构造函数包装 |
此外,对于异步函数中的错误处理,要合理使用
try-catch
块和
await
关键字。在使用
await
时,要确保正确处理可能的拒绝情况。对于微任务和任务的调度,要理解它们的执行顺序和优先级,避免因微任务调度不当导致事件循环陷入无限循环。
9. 流程图示例
下面是一个简单的流程图,展示了异步函数中错误处理的流程:
graph TD;
A[开始异步函数] --> B{Promise 是否被拒绝};
B -- 是 --> C[跳转到 catch 块];
B -- 否 --> D[继续执行 await 之后的代码];
C --> E[处理错误];
D --> F[完成异步函数];
E --> F;
这个流程图清晰地展示了在异步函数中,如果
await
的 Promise 被拒绝,执行流程会跳转到
catch
块进行错误处理;如果 Promise 被解决,则继续执行
await
之后的代码。
通过遵循这些最佳实践和理解异步编程的原理,可以编写出更健壮、高效且易于维护的 JavaScript 异步代码。
超级会员免费看
793

被折叠的 条评论
为什么被折叠?



