从回调地狱到Promise链,再到async/await的同步写法,JavaScript异步编程的进化之路
大家好,我是你们的技术小伙伴FogLetter,今天我们来聊聊JavaScript异步编程的那些事儿。作为一个在掘金上分享过不少技术文章的老司机,我深知异步编程是每个JS开发者都必须掌握的技能。让我们一起来探索Promise和async/await的奇妙世界吧!
异步编程的进化史
还记得早期的JavaScript异步编程吗?满屏的回调函数,嵌套层级深不见底:
getData(function(a) {
getMoreData(a, function(b) {
getMoreData(b, function(c) {
getMoreData(c, function(d) {
getMoreData(d, function(e) {
// 这就是传说中的"回调地狱"
});
});
});
});
});
这种代码不仅难以阅读,错误处理也极其困难。然后Promise出现了,给我们带来了曙光。
Promise:异步编程的基石
什么是Promise?
Promise是JavaScript中处理异步操作的对象,它代表一个尚未完成但预期会完成的操作。简单来说,Promise就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。
Promise有三种状态:
- pending:初始状态,既不是成功,也不是失败
- fulfilled:操作成功完成
- rejected:操作失败
状态一旦改变,就不会再变:只能从pending变为fulfilled或者从pending变为rejected。
Promise的基本用法
const promise = new Promise((resolve, reject) => {
// 异步操作
setTimeout(() => {
const random = Math.random();
if (random > 0.5) {
resolve('操作成功:' + random);
} else {
reject('操作失败:' + random);
}
}, 1000);
});
promise.then(result => {
console.log(result);
}).catch(error => {
console.error(error);
});
Promise的并行处理:all、race、any、allSettled
Promise真正强大的地方在于它提供了一系列处理多个异步操作的静态方法。让我们通过一个生动的例子来理解它们:
// 模拟几个异步操作
const p1 = Promise.resolve('p1立即完成');
const p2 = new Promise((resolve) => {
setTimeout(() => resolve('p2 1秒后完成'), 1000);
});
const p3 = new Promise((resolve) => {
setTimeout(() => resolve('p3 2秒后完成'), 2000);
});
const p4 = Promise.reject('p4立即拒绝');
const p5 = new Promise((resolve, reject) => {
setTimeout(() => reject('p5 1.5秒后拒绝'), 1500);
});
1. Promise.all() - 全成功才成功
console.time('Promise.all总耗时');
Promise.all([p1, p2, p3])
.then(results => {
console.log('全部成功:', results);
console.timeEnd('Promise.all总耗时');
})
.catch(error => {
console.log('有一个失败:', error);
console.timeEnd('Promise.all总耗时');
});
// 输出:
// 全部成功: ["p1立即完成", "p2 1秒后完成", "p3 2秒后完成"]
// Promise.all总耗时: 2002.345947265625ms
特点:
- 所有Promise都成功,才成功
- 任何一个失败,立即失败
- 适合多个异步操作都成功才能继续的场景
2. Promise.race() - 谁快听谁的
Promise.race([p2, p3, p5])
.then(result => {
console.log('第一个完成的是:', result);
})
.catch(error => {
console.log('第一个失败的是:', error);
});
// 输出:第一个完成的是: p2 1秒后完成
特点:
- 哪个Promise最先完成(无论成功或失败),就采用它的结果
- 适合设置超时机制
3. Promise.any() - 首个成功即成功
Promise.any([p4, p5, p2])
.then(result => {
console.log('第一个成功的是:', result);
})
.catch(errors => {
console.log('全部失败了:', errors);
});
// 输出:第一个成功的是: p2 1秒后完成
特点:
- 只要有一个成功,就成功
- 只有全部失败,才失败
- 适合多个备用方案,只要一个成功即可的场景
4. Promise.allSettled() - 全部完成才结束
Promise.allSettled([p1, p4, p5])
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`p${index+1}成功:`, result.value);
} else {
console.log(`p${index+1}失败:`, result.reason);
}
});
});
// 输出:
// p1成功: p1立即完成
// p2失败: p4立即拒绝
// p3失败: p5 1.5秒后拒绝
特点:
- 等待所有Promise完成(无论成功或失败)
- 返回每个Promise的结果数组
- 适合需要知道所有操作结果的场景
手写实现Promise.all
理解了Promise.all的用法,我们来尝试自己实现一个:
Promise.MyAll = function(promises) {
let results = []; // 存储每个Promise的结果
let completed = 0; // 已完成的数量
return new Promise((resolve, reject) => {
promises.forEach((promise, index) => {
// 用Promise.resolve包装,确保处理的是Promise对象
Promise.resolve(promise)
.then(result => {
results[index] = result; // 按顺序存储结果
completed++;
// 全部完成,解析结果
if (completed === promises.length) {
resolve(results);
}
})
.catch(error => {
// 任何一个失败,立即拒绝
reject(error);
});
});
// 处理空数组的情况
if (promises.length === 0) {
resolve(results);
}
});
};
// 测试我们实现的MyAll
Promise.MyAll([p1, p2, p3])
.then(results => {
console.log('MyAll结果:', results);
})
.catch(error => {
console.log('MyAll错误:', error);
});
async/await:异步编程的终极解决方案
虽然Promise解决了回调地狱的问题,但then()链式调用仍然不够直观。ES2017引入了async/await,让异步代码看起来像同步代码一样简洁。
什么是async/await?
async/await是基于Promise的语法糖,它允许我们以同步的方式编写异步代码。
- async:声明一个函数是异步的,该函数总会返回一个Promise
- await:暂停async函数的执行,等待Promise解决,然后继续执行
基本用法
// 传统Promise写法
function fetchData() {
return fetch('/api/data')
.then(response => response.json())
.then(data => {
return processData(data);
})
.catch(error => {
console.error('错误:', error);
});
}
// async/await写法
async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return processData(data);
} catch (error) {
console.error('错误:', error);
}
}
是不是清晰多了?async/await让代码可读性大大提升。
async/await的实现原理
你可能想知道async/await是如何工作的。本质上,它是Generator函数和Promise的组合:
// 模拟async函数的实现
function asyncGenerator(genFunc) {
return function() {
const generator = genFunc.apply(this, arguments);
return new Promise((resolve, reject) => {
function step(key, arg) {
let result;
try {
result = generator[key](arg);
} catch (error) {
return reject(error);
}
const { value, done } = result;
if (done) {
return resolve(value);
} else {
return Promise.resolve(value).then(
val => step('next', val),
err => step('throw', err)
);
}
}
step('next');
});
};
}
// 使用示例
const myAsync = asyncGenerator(function* () {
const a = yield Promise.resolve(1);
const b = yield Promise.resolve(2);
return a + b;
});
myAsync().then(result => console.log(result)); // 输出: 3
当然,实际实现比这复杂得多,但基本原理类似。Babel等转译器就是将async/await转换为类似的代码。
不要滥用async/await
虽然async/await很强大,但不能滥用。最常见的错误是在不需要顺序执行时也使用await:
// 错误用法 - 顺序执行,耗时更长
async function fetchSequential() {
const user = await fetchUser(); // 等待用户数据
const posts = await fetchPosts(); // 然后等待文章数据
const comments = await fetchComments(); // 然后等待评论数据
return { user, posts, comments };
}
// 正确用法 - 并行执行,效率更高
async function fetchParallel() {
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
]);
return { user, posts, comments };
}
经验法则:
- 多个异步操作有依赖关系时,使用async/await顺序执行
- 多个异步操作相互独立时,使用Promise.all并行执行
实战技巧与常见陷阱
1. 错误处理
async/await的错误处理有多种方式:
// 方式1:try/catch
async function fetchData() {
try {
const data = await fetch('/api/data');
return processData(data);
} catch (error) {
console.error('获取数据失败:', error);
// 处理错误或重新抛出
throw new Error('数据处理失败');
}
}
// 方式2:catch方法
async function fetchData() {
const data = await fetch('/api/data').catch(error => {
console.error('获取数据失败:', error);
return fallbackData; // 返回备用数据
});
return processData(data);
}
// 方式3:在调用处处理
async function main() {
const dataPromise = fetchData();
// 其他操作...
try {
const data = await dataPromise;
console.log('数据:', data);
} catch (error) {
console.error('操作失败:', error);
}
}
2. 循环中的await
在循环中使用await需要注意:
// 顺序执行 - 每个await等待前一个完成
async function processSequentially(items) {
for (const item of items) {
await processItem(item); // 顺序处理
}
}
// 并行执行 - 同时处理所有项目
async function processInParallel(items) {
const promises = items.map(item => processItem(item));
await Promise.all(promises); // 并行处理
}
3. 顶层await
在ES2022中,我们可以在模块的顶层使用await:
// 模块加载时等待数据加载
const data = await fetch('/api/data').then(r => r.json());
export function getData() {
return data;
}
总结
Promise和async/await是JavaScript异步编程的强大工具:
- Promise 提供了处理异步操作的标准方法和并行处理能力
- async/await 让异步代码看起来像同步代码,提高了可读性和可维护性
- 根据场景选择合适的工具:有依赖关系的异步操作使用async/await,独立的异步操作使用Promise.all
记住,技术是为人服务的,选择最适合当前场景的方法,而不是盲目追求最新特性。希望这篇文章能帮助你更好地理解和使用Promise和async/await!
如果你有任何问题或想法,欢迎在评论区留言讨论。记得点赞收藏哦,我们下期再见!
互动环节:你在使用Promise或async/await时遇到过什么有趣的问题或坑?分享在评论区,我们一起讨论解决方案!