跳到主要内容

ES6异步编程详解:Promise、async/await与异步流程控制

阅读需 9 分钟
wqz

JavaScript作为一门单线程语言,异步编程是其核心特性之一。ES6(ECMAScript 2015)及后续版本引入了多种新的异步编程方式,极大地改善了JavaScript的异步处理能力。本文将详细介绍ES6中的异步编程特性,帮助你掌握现代JavaScript异步流程控制的核心概念和实践技巧。

1. 异步编程基础

在深入学习ES6的异步特性之前,我们需要理解什么是异步编程以及为什么它在JavaScript中如此重要。

什么是异步操作?

在编程中,操作可以分为两种类型:

  • 同步操作:代码按顺序执行,前一个任务完成后才执行下一个任务
  • 异步操作:不等待当前任务完成,就执行下一个任务(比如定时器、网络请求等)

JavaScript是单线程的,这意味着它一次只能执行一个任务。如果某个任务需要很长时间(如网络请求),同步执行会导致整个程序"卡住",用户界面无响应。异步编程解决了这个问题,允许程序在等待耗时操作完成的同时继续执行其他代码。

传统的异步处理方式:回调函数

在ES6之前,JavaScript主要通过回调函数处理异步操作:

// 使用回调函数处理异步操作
function fetchData(callback) {
setTimeout(() => {
const data = { name: "JavaScript", type: "编程语言" };
callback(data);
}, 2000);
}

fetchData((data) => {
console.log("数据获取成功:", data);
// 可能需要进行下一步异步操作...
});

console.log("请求已发送,等待数据返回...");

回调函数虽然简单,但当多个异步操作需要按顺序执行时,会导致"回调地狱"(Callback Hell):

fetchUserData((userData) => {
fetchUserPosts(userData.id, (posts) => {
fetchPostComments(posts[0].id, (comments) => {
fetchCommentAuthor(comments[0].authorId, (author) => {
// 嵌套层级越来越深,代码难以维护
console.log(author);
});
});
});
});

2. Promise - 处理异步操作的新方式

ES6引入的Promise对象为异步编程提供了更优雅的解决方案。

2.1 Promise 是什么?

Promise是一个代表异步操作最终完成或失败的对象。它就像一个"承诺":代表一个尚未完成但预期将来会完成的操作。

想象你去快餐店点餐:

  • 你下单后,店员给你一个取餐号(这就是Promise)
  • 你可以先去找座位(继续执行其他代码)
  • 当餐准备好了,取餐号会通知你(Promise完成)
  • 如果食材缺货,取餐号也会通知你(Promise失败)

2.2 Promise 的三种状态

Promise有三种状态:

  1. pending(进行中):初始状态,表示操作正在进行中
  2. fulfilled(已成功):操作成功完成
  3. rejected(已失败):操作失败

一旦Promise状态改变(从pending到fulfilled或rejected),就会永久保持该状态,不会再变化。

2.3 创建和使用Promise

创建Promise

// 创建一个Promise
const myPromise = new Promise((resolve, reject) => {
// 异步操作
setTimeout(() => {
const success = Math.random() > 0.5; // 随机成功或失败
if (success) {
resolve('操作成功!'); // 成功时调用resolve
} else {
reject('操作失败!'); // 失败时调用reject
}
}, 2000);
});

使用Promise

myPromise
.then((result) => {
// 处理成功情况
console.log(result); // "操作成功!"
})
.catch((error) => {
// 处理失败情况
console.error(error); // "操作失败!"
})
.finally(() => {
// 无论成功或失败都会执行
console.log('Promise已完成');
});

2.4 Promise链式调用

Promise的一个强大特性是可以链式调用,避免回调地狱:

// 使用Promise重写前面的嵌套回调示例
fetchUserData()
.then(userData => fetchUserPosts(userData.id))
.then(posts => fetchPostComments(posts[0].id))
.then(comments => fetchCommentAuthor(comments[0].authorId))
.then(author => {
console.log(author);
})
.catch(error => {
console.error("发生错误:", error);
});

每个.then()返回一个新的Promise,使得异步操作可以串联起来。

2.5 Promise并行执行

当需要同时执行多个异步操作时,可以使用Promise.all()Promise.race()等方法:

Promise.all()

等待所有Promise完成(或第一个失败):

const promise1 = fetchUserData();
const promise2 = fetchUserPosts();
const promise3 = fetchWeatherData();

Promise.all([promise1, promise2, promise3])
.then(([userData, posts, weather]) => {
console.log("所有数据都已获取:", userData, posts, weather);
})
.catch(error => {
console.error("至少一个请求失败:", error);
});

Promise.race()

返回最先完成(或失败)的Promise结果:

const promise1 = fetchFromServer1(); // 可能需要3秒
const promise2 = fetchFromServer2(); // 可能需要1秒

Promise.race([promise1, promise2])
.then(result => {
console.log("最快的服务器返回了:", result); // 通常是server2的结果
})
.catch(error => {
console.error("最快的请求失败了:", error);
});

Promise.allSettled()

等待所有Promise完成(无论成功或失败):

Promise.allSettled([promise1, promise2, promise3])
.then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('成功:', result.value);
} else {
console.log('失败:', result.reason);
}
});
});

3. async/await - 更优雅的异步处理

ES2017引入的async/await是基于Promise的语法糖,让异步代码看起来更像同步代码,更易于理解和维护。

3.1 async函数

async关键字用于声明一个异步函数,该函数会自动返回一个Promise:

// 普通函数
function normalFunction() {
return "Hello";
}
console.log(normalFunction()); // "Hello"

// 异步函数
async function asyncFunction() {
return "Hello";
}
console.log(asyncFunction()); // Promise {<fulfilled>: "Hello"}
asyncFunction().then(result => console.log(result)); // "Hello"

3.2 await表达式

await关键字只能在async函数内部使用,它会暂停函数执行,等待Promise解决,然后返回Promise的结果:

async function fetchUserData() {
try {
// await会暂停函数执行,直到Promise完成
const response = await fetch('https://api.example.com/user');
const userData = await response.json();

// 获取用户帖子
const postsResponse = await fetch(`https://api.example.com/posts?userId=${userData.id}`);
const posts = await postsResponse.json();

return { user: userData, posts: posts };
} catch (error) {
console.error("获取数据时出错:", error);
throw error; // 重新抛出错误
}
}

// 使用异步函数
fetchUserData()
.then(data => console.log("获取的数据:", data))
.catch(error => console.error("处理错误:", error));

3.3 错误处理

async/await中,可以使用传统的try/catch语句处理错误,使代码更加清晰:

async function processData() {
try {
const data = await fetchData();
const processed = await processResult(data);
return processed;
} catch (error) {
console.error("处理数据时出错:", error);
// 可以返回默认值或重新抛出错误
return defaultValue;
} finally {
// 清理代码,无论成功或失败都会执行
cleanup();
}
}

3.4 并行执行异步操作

使用async/await时,如果多个异步操作之间没有依赖关系,应该并行执行它们以提高性能:

async function fetchAllData() {
try {
// 并行执行所有fetch操作
const [userResponse, postsResponse, weatherResponse] = await Promise.all([
fetch('https://api.example.com/user'),
fetch('https://api.example.com/posts'),
fetch('https://api.example.com/weather')
]);

// 并行执行所有json解析
const [user, posts, weather] = await Promise.all([
userResponse.json(),
postsResponse.json(),
weatherResponse.json()
]);

return { user, posts, weather };
} catch (error) {
console.error("获取数据时出错:", error);
throw error;
}
}

4. Generator函数与异步迭代

ES6引入的Generator函数提供了另一种处理异步操作的方式,尤其在ES2018引入异步迭代器后更加强大。

4.1 Generator基础

Generator函数使用function*语法,可以通过yield关键字暂停和恢复执行:

function* simpleGenerator() {
console.log('开始执行');
yield 1;
console.log('第一次暂停后继续');
yield 2;
console.log('第二次暂停后继续');
return 3;
}

const gen = simpleGenerator();
console.log(gen.next()); // {value: 1, done: false}
console.log(gen.next()); // {value: 2, done: false}
console.log(gen.next()); // {value: 3, done: true}

4.2 使用Generator处理异步操作

在Promise出现之前,Generator被用来处理异步操作:

function fetchData() {
return new Promise(resolve => {
setTimeout(() => resolve("数据"), 1000);
});
}

function* fetchGenerator() {
console.log("开始获取数据");
const data = yield fetchData();
console.log("获取的数据:", data);
return "完成";
}

const gen = fetchGenerator();
const promise = gen.next().value; // 获取第一个yield的Promise

promise.then(data => {
const result = gen.next(data); // 将Promise结果传回Generator
console.log("Generator返回:", result.value);
});

4.3 异步迭代器

ES2018引入的异步迭代器使处理异步数据流更加简单:

// 创建一个异步可迭代对象
const asyncIterable = {
[Symbol.asyncIterator]() {
let i = 0;
return {
next() {
if (i < 5) {
return new Promise(resolve => {
setTimeout(() => {
resolve({ value: i++, done: false });
}, 1000);
});
}
return Promise.resolve({ done: true });
}
};
}
};

// 使用for await...of遍历异步可迭代对象
async function consumeAsyncIterable() {
for await (const num of asyncIterable) {
console.log(num); // 每隔1秒输出0, 1, 2, 3, 4
}
console.log('迭代完成');
}

consumeAsyncIterable();

5. 实际应用示例

5.1 使用Promise处理API请求

// 封装fetch API
function fetchJSON(url) {
return fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
return response.json();
});
}

// 使用Promise链
fetchJSON('https://api.example.com/users')
.then(users => {
console.log('获取到用户列表:', users);
return fetchJSON(`https://api.example.com/users/${users[0].id}/posts`);
})
.then(posts => {
console.log('获取到帖子列表:', posts);
return fetchJSON(`https://api.example.com/posts/${posts[0].id}/comments`);
})
.then(comments => {
console.log('获取到评论列表:', comments);
})
.catch(error => {
console.error('请求过程中出错:', error);
});

5.2 使用async/await实现相同功能

async function fetchUserPostsComments() {
try {
const users = await fetchJSON('https://api.example.com/users');
console.log('获取到用户列表:', users);

const posts = await fetchJSON(`https://api.example.com/users/${users[0].id}/posts`);
console.log('获取到帖子列表:', posts);

const comments = await fetchJSON(`https://api.example.com/posts/${posts[0].id}/comments`);
console.log('获取到评论列表:', comments);

return comments;
} catch (error) {
console.error('请求过程中出错:', error);
throw error;
}
}

fetchUserPostsComments()
.then(() => console.log('所有数据获取完成'))
.catch(error => console.error('最终错误处理:', error));

5.3 实现超时控制

function fetchWithTimeout(url, timeout = 5000) {
return Promise.race([
fetch(url),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('请求超时')), timeout)
)
]);
}

async function fetchData() {
try {
const response = await fetchWithTimeout('https://api.example.com/data', 3000);
const data = await response.json();
console.log('数据:', data);
} catch (error) {
if (error.message === '请求超时') {
console.error('请求超过3秒未完成');
} else {
console.error('请求失败:', error);
}
}
}

总结

ES6及后续版本极大地改进了JavaScript的异步编程能力:

  1. Promise提供了一种更优雅的方式处理异步操作,避免了回调地狱
  2. async/await让异步代码看起来更像同步代码,提高了可读性和可维护性
  3. Generator和异步迭代器为处理复杂的异步数据流提供了强大工具

掌握这些异步编程技术对于现代JavaScript开发至关重要,它们能帮助你编写更高效、更可靠的异步代码。随着你对这些概念的熟悉,你将能够更自信地处理各种异步编程挑战。

参考资源

分享这篇文章
Loading Comments...