异步 JavaScript:Promise、Async/Await 深入解析
在 JavaScript 中,异步编程是一个核心概念,它允许我们在不阻塞主线程的情况下处理耗时操作。本文将深入探讨 Promise 链、Promise 静态方法以及 Async/Await 语法,帮助你更好地理解和运用异步编程。
1. Promise 链与错误处理
Promise 链是处理异步操作序列的一种方式。当一个 Promise 被解决或拒绝时,会触发链中的下一个 then 或 catch 方法。
// 示例代码
fetch(url1)
.then(parseFetchResponse)
.then((data1) => {
console.log(data1);
return fetch(url2);
})
.then(parseFetchResponse)
.then((data2) => {
console.log(data2);
return fetch(url3);
})
.then(parseFetchResponse)
.then((data3) => {
console.log(data3);
})
.catch((error) => {
console.log(error.message);
});
function parseFetchResponse(response) {
if (response.ok) {
return response.json();
} else {
throw new Error("request failed");
}
}
在这个示例中,我们依次发起三个 HTTP 请求,每个请求完成后解析响应数据。如果任何一个请求失败,会触发 catch 方法处理错误。
错误处理:then 与 catch 的区别
在 then 方法中可以通过传递第二个参数来注册拒绝处理程序,但要注意,这个拒绝处理程序只有在原始 Promise 被拒绝时才会被调用。如果 then 方法返回的 Promise 被拒绝,该拒绝处理程序不会被调用。
fakeRequest().then(
(response) => {
throw new Error("error");
},
(error) => {
console.log(error.message); // 不会被调用
}
);
为了避免未处理的 Promise 拒绝,建议始终使用 catch 方法来处理可能的错误。
2. Promise 静态方法的常见用例
Promise 有几个静态方法,其中 Promise.all 和 Promise.race 是最常用的。
2.1 并发请求:Promise.all
当我们需要同时发起多个独立的 HTTP 请求并等待所有请求完成时,可以使用 Promise.all 方法。
const url1 = "https://jsonplaceholder.typicode.com/todos/1";
const url2 = "https://jsonplaceholder.typicode.com/todos/2";
const url3 = "https://jsonplaceholder.typicode.com/todos/3";
Promise.all([
fetch(url1).then(parseFetchResponse),
fetch(url2).then(parseFetchResponse),
fetch(url3).then(parseFetchResponse)
])
.then((dataArr) => {
console.log(dataArr);
})
.catch((error) => console.log(error.message));
Promise.all 接受一个可迭代对象(如数组)作为输入,返回一个新的 Promise。当所有输入的 Promise 都被解决时,返回的 Promise 才会被解决,其解决值是一个包含所有输入 Promise 解决值的数组。如果任何一个输入的 Promise 被拒绝,返回的 Promise 会立即被拒绝。
2.2 请求超时:Promise.race
在某些情况下,我们不希望一个 HTTP 请求长时间处于挂起状态。可以使用 Promise.race 方法实现请求超时功能。
function delayedRequest() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("hello world");
}, 8000);
});
}
function timeout() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const error = new Error("request timed out");
reject(error);
}, 3000);
});
}
Promise.race([delayedRequest(), timeout()])
.then((response) => {
console.log(response);
})
.catch((error) => console.log(error.message));
Promise.race 同样接受一个可迭代对象作为输入,返回一个新的 Promise。当输入的 Promise 中有一个被解决或拒绝时,返回的 Promise 会立即以相同的状态被解决或拒绝。
3. Async/Await 语法
虽然 Promise 解决了回调地狱和错误处理的问题,但使用回调函数来注册解决和拒绝处理程序仍然显得冗长。Async/Await 语法是一种更简洁、直观的处理 Promise 的方式。
3.1 基本用法
使用 Async/Await 语法需要遵循两个主要步骤:
1. 使用 async 关键字标记函数,因为 await 关键字只能在 async 函数中使用。
2. 在 async 函数内部使用 await 关键字等待 Promise 解决。
async function fetchTodo(url) {
try {
const response = await fetch(url);
if (response.ok) {
const data = await response.json();
console.log(data);
} else {
throw new Error("request failed");
}
} catch (error) {
console.log(error.message);
}
}
const url = "https://jsonplaceholder.typicode.com/todos/1";
fetchTodo(url);
在这个示例中,我们使用 Async/Await 重写了之前的 fetchTodo 函数。代码看起来更像同步代码,但实际上是异步执行的。
3.2 async 函数
async 函数总是返回一个 Promise。函数内部的返回值会被包装在一个已解决的 Promise 中,而抛出的错误会导致 Promise 被拒绝。
async function foo() {
return 123;
}
foo().then(console.log); // 123
async function bar() {
throw new Error("some error occurred");
}
bar().catch((error) => console.log(error.message)); // some error occurred
3.3 await 关键字
await 关键字用于等待 Promise 解决。它会暂停 async 函数的执行,直到 Promise 被解决或拒绝。
async function example() {
const response = await fetch(url);
const data = await response.json();
console.log(data);
}
在这个示例中,await 表达式会直接计算 Promise 的解决值,我们可以将其保存到变量中。与 Promise 链不同,不需要显式注册解决处理程序。
4. 多个 await 表达式
在 async 函数中可以使用多个 await 表达式,但要注意它们是按顺序执行的,不会并行执行。
function promisifiedRandomNumber() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const randomNum = Math.floor(Math.random() * 10);
resolve(randomNum);
}, 1000);
});
}
async function random() {
const num1 = await promisifiedRandomNumber();
const num2 = await promisifiedRandomNumber();
const num3 = await promisifiedRandomNumber();
console.log(num1, num2, num3);
}
random();
在这个示例中,每个 await 表达式都会等待大约 1 秒,整个函数执行大约需要 3 秒。
如果需要并发执行多个异步操作,可以使用 Promise.all 方法。
async function random() {
const promiseArr = [
promisifiedRandomNumber(),
promisifiedRandomNumber(),
promisifiedRandomNumber()
];
const randomNumsArr = await Promise.all(promiseArr);
console.log(randomNumsArr);
}
random();
5. 错误处理
在 async 函数中,可以使用 try-catch 块来处理 Promise 拒绝。
async function getUsersAndTasks() {
try {
const users = await fetchUsers();
const tasks = await fetchTasks();
} catch (error) {
// 处理错误
}
}
通过这种方式,我们可以捕获并处理任何异步操作中抛出的错误。
总结
本文介绍了 JavaScript 中异步编程的重要概念,包括 Promise 链、Promise 静态方法和 Async/Await 语法。掌握这些知识可以帮助我们编写更简洁、可维护的异步代码。以下是一些关键点总结:
- 使用 Promise 链按顺序处理多个异步操作,并使用 catch 方法处理错误。
- 使用 Promise.all 并发执行多个独立的异步操作。
- 使用 Promise.race 实现请求超时功能。
- 使用 Async/Await 语法以更简洁、直观的方式处理 Promise。
- 在 async 函数中使用 try-catch 块处理可能的错误。
希望这些内容对你理解和运用 JavaScript 异步编程有所帮助。
异步 JavaScript:Promise、Async/Await 深入解析
6. 异步编程的实际应用场景
异步编程在实际开发中有广泛的应用场景,下面将介绍几个常见的场景及如何运用上述知识解决问题。
6.1 数据加载与渲染
在前端开发中,经常需要从服务器获取数据并渲染到页面上。使用异步编程可以避免页面卡顿,提高用户体验。
async function loadDataAndRender() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
// 渲染数据到页面
renderData(data);
} catch (error) {
console.log('数据加载失败:', error.message);
}
}
function renderData(data) {
// 实现数据渲染逻辑
const container = document.getElementById('data-container');
container.innerHTML = JSON.stringify(data);
}
loadDataAndRender();
在这个示例中,我们使用 Async/Await 从服务器获取数据,然后调用
renderData
函数将数据渲染到页面上。如果数据加载失败,会在控制台输出错误信息。
6.2 多资源加载
当需要同时加载多个资源(如图片、脚本等)时,可以使用
Promise.all
并发加载。
function loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error('图片加载失败'));
img.src = url;
});
}
async function loadMultipleImages() {
const imageUrls = [
'https://example.com/image1.jpg',
'https://example.com/image2.jpg',
'https://example.com/image3.jpg'
];
const promises = imageUrls.map(url => loadImage(url));
try {
const images = await Promise.all(promises);
// 所有图片加载完成后处理
images.forEach(img => document.body.appendChild(img));
} catch (error) {
console.log('图片加载出错:', error.message);
}
}
loadMultipleImages();
在这个示例中,我们定义了
loadImage
函数用于加载单个图片,返回一个 Promise。然后使用
Promise.all
并发加载多个图片,当所有图片加载完成后,将它们添加到页面上。
7. 异步编程的性能优化
在异步编程中,合理的性能优化可以提高程序的响应速度和资源利用率。
7.1 避免不必要的等待
在使用多个
await
表达式时,要确保它们是必要的顺序执行。如果某些操作可以并发执行,应使用
Promise.all
或
Promise.race
。
// 不好的示例
async function badExample() {
const result1 = await someAsyncOperation1();
const result2 = await someAsyncOperation2();
return [result1, result2];
}
// 好的示例
async function goodExample() {
const promise1 = someAsyncOperation1();
const promise2 = someAsyncOperation2();
const [result1, result2] = await Promise.all([promise1, promise2]);
return [result1, result2];
}
在
badExample
中,
someAsyncOperation2
必须等待
someAsyncOperation1
完成后才能开始执行。而在
goodExample
中,两个操作并发执行,提高了性能。
7.2 控制并发数量
当需要处理大量异步任务时,可能会导致资源耗尽。可以使用队列来控制并发数量。
function asyncTask(task) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(task);
}, Math.random() * 1000);
});
}
async function limitConcurrency(tasks, limit) {
const queue = [...tasks];
const running = [];
const results = [];
while (queue.length > 0 || running.length > 0) {
while (running.length < limit && queue.length > 0) {
const task = queue.shift();
const promise = asyncTask(task).then(result => {
results.push(result);
running.splice(running.indexOf(promise), 1);
});
running.push(promise);
}
await Promise.race(running);
}
return results;
}
const tasks = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
limitConcurrency(tasks, 3).then(results => {
console.log('任务完成:', results);
});
在这个示例中,我们定义了
limitConcurrency
函数,它接受一个任务数组和并发限制数量作为参数。通过队列和
Promise.race
控制并发数量,确保同时执行的任务不超过指定数量。
8. 异步编程的流程图
为了更好地理解异步编程的执行流程,下面是一个简单的 Async/Await 执行流程图:
graph TD;
A[调用 async 函数] --> B[同步执行代码];
B --> C{是否遇到 await};
C -- 是 --> D[暂停函数执行];
D --> E[等待 Promise 解决];
E -- 解决 --> F[恢复函数执行];
F --> G[继续执行后续代码];
C -- 否 --> G;
G --> H{是否还有 await};
H -- 是 --> C;
H -- 否 --> I[函数执行结束];
这个流程图展示了 Async/Await 函数的执行过程。当遇到
await
表达式时,函数执行会暂停,等待 Promise 解决后再继续执行。
9. 总结与展望
异步编程是 JavaScript 中非常重要的一部分,它允许我们处理耗时操作而不阻塞主线程。通过 Promise 链、Promise 静态方法和 Async/Await 语法,我们可以编写更简洁、可维护的异步代码。
在未来的开发中,异步编程的应用场景会越来越广泛,同时也会有更多的工具和技术出现来简化异步编程。例如,ES2022 引入了顶级 await,允许在模块的顶层使用
await
关键字,进一步简化了异步代码的编写。
希望本文能帮助你深入理解 JavaScript 异步编程,并在实际项目中灵活运用这些知识。不断学习和实践,你将能够更好地掌握异步编程,提高代码的质量和性能。
总结
本文全面介绍了 JavaScript 异步编程的核心知识,从 Promise 链、Promise 静态方法到 Async/Await 语法,再到实际应用场景和性能优化。以下是关键知识点总结:
1.
Promise 链
:用于按顺序处理多个异步操作,使用
catch
方法处理错误。
2.
Promise 静态方法
:
-
Promise.all
:并发执行多个独立的异步操作。
-
Promise.race
:实现请求超时功能。
3.
Async/Await 语法
:以更简洁、直观的方式处理 Promise,使用
try-catch
块处理错误。
4.
实际应用场景
:数据加载与渲染、多资源加载等。
5.
性能优化
:避免不必要的等待,控制并发数量。
掌握这些知识,你可以编写高效、可维护的异步代码,提升 JavaScript 编程能力。
超级会员免费看

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



