一、异步编程的痛点:为什么你的代码总是不听话?
兄弟们,不知道你们有没有遇到过这种情况:明明代码逻辑看起来没问题,但执行顺序就是乱七八糟的。比如你想先获取用户数据,再根据数据渲染页面,结果页面先出来了,数据却迟迟不来。这就是典型的异步问题,就像你去餐厅点餐,服务员说"马上来",结果你等到菜都凉了还没上齐。
在JavaScript的世界里,这种问题尤其常见。因为JS是单线程的,为了不阻塞主线程,很多操作都是异步执行的。比如网络请求、文件读写、定时器等等。下面这个例子就很典型:
// 技术栈:Node.js
console.log('1. 开始点餐');
setTimeout(() => {
console.log('3. 服务员说:您的菜马上来');
}, 1000);
console.log('2. 等待上菜');
// 输出顺序:
// 1. 开始点餐
// 2. 等待上菜
// 3. 服务员说:您的菜马上来
看到没?明明setTimeout写在中间,却最后执行。这就是异步的"魔力"。
二、回调地狱:代码里的俄罗斯套娃
最早我们解决异步问题用的是回调函数,结果一不小心就写出了"回调地狱"。看看这个例子:
// 技术栈:Node.js
const fs = require('fs');
// 读取第一个文件
fs.readFile('file1.txt', 'utf8', (err, data1) => {
if (err) throw err;
console.log(data1);
// 读取第二个文件
fs.readFile('file2.txt', 'utf8', (err, data2) => {
if (err) throw err;
console.log(data2);
// 读取第三个文件
fs.readFile('file3.txt', 'utf8', (err, data3) => {
if (err) throw err;
console.log(data3);
// 还能继续嵌套...
});
});
});
这种代码就像俄罗斯套娃,一层套一层,看得人头晕眼花。更可怕的是错误处理,每个回调都要单独处理错误,代码量直接翻倍。
三、Promise:异步编程的救星
后来Promise出现了,它就像个承诺,告诉你"我现在可能没结果,但将来一定有"。看看用Promise改造后的代码:
// 技术栈:Node.js
const fs = require('fs').promises;
// 用Promise链式调用
fs.readFile('file1.txt', 'utf8')
.then(data1 => {
console.log(data1);
return fs.readFile('file2.txt', 'utf8');
})
.then(data2 => {
console.log(data2);
return fs.readFile('file3.txt', 'utf8');
})
.then(data3 => {
console.log(data3);
})
.catch(err => {
// 统一错误处理
console.error('出错啦:', err);
});
这下清爽多了吧?Promise的链式调用让代码变成了从上到下的顺序,错误处理也只需要一个catch就够了。
Promise还有几个实用技巧:
Promise.all:等所有Promise都完成
Promise.all([
fs.readFile('file1.txt', 'utf8'),
fs.readFile('file2.txt', 'utf8'),
fs.readFile('file3.txt', 'utf8')
]).then(([data1, data2, data3]) => {
console.log('所有文件都读完了');
}).catch(err => {
console.error('有文件读取失败', err);
});
Promise.race:看哪个Promise最先完成
Promise.race([
fetch('https://api1.com'),
fetch('https://api2.com'),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('超时')), 5000)
)
]).then(response => {
console.log('最快的API响应:', response);
}).catch(err => {
console.error('出错或超时:', err);
});
四、async/await:让异步代码看起来像同步
虽然Promise已经很好用了,但ES2017又给我们带来了async/await这个语法糖,让异步代码写起来跟同步代码一样直观。
// 技术栈:Node.js
const fs = require('fs').promises;
async function readFiles() {
try {
const data1 = await fs.readFile('file1.txt', 'utf8');
console.log(data1);
const data2 = await fs.readFile('file2.txt', 'utf8');
console.log(data2);
const data3 = await fs.readFile('file3.txt', 'utf8');
console.log(data3);
} catch (err) {
console.error('出错啦:', err);
}
}
readFiles();
是不是更直观了?async/await本质上还是基于Promise的,但代码可读性大大提升。不过要注意几个点:
- await只能在async函数中使用
- async函数总是返回一个Promise
- 错误处理要用try-catch
再来个更实用的例子,结合API请求:
// 技术栈:浏览器环境
async function getUserData(userId) {
try {
// 第一个请求:获取用户基本信息
const userResponse = await fetch(`/api/users/${userId}`);
const user = await userResponse.json();
// 第二个请求:获取用户订单
const ordersResponse = await fetch(`/api/orders?userId=${userId}`);
const orders = await ordersResponse.json();
// 第三个请求:获取用户收藏
const favoritesResponse = await fetch(`/api/favorites/${userId}`);
const favorites = await favoritesResponse.json();
return {
...user,
orders,
favorites
};
} catch (error) {
console.error('获取用户数据失败:', error);
throw error; // 继续抛出错误让调用方处理
}
}
// 使用示例
(async () => {
try {
const userData = await getUserData(123);
console.log('用户完整数据:', userData);
} catch {
console.log('显示错误页面');
}
})();
五、高级技巧:让异步代码更健壮
在实际项目中,我们还需要考虑更多场景。比如:
- 超时控制
function withTimeout(promise, timeout) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('操作超时')), timeout)
)
]);
}
// 使用示例
async function fetchWithTimeout() {
try {
const response = await withTimeout(
fetch('https://api.example.com/data'),
3000 // 3秒超时
);
console.log('成功获取数据');
} catch (err) {
console.error('请求失败:', err.message);
}
}
- 自动重试
async function retry(fn, retries = 3, delay = 1000) {
try {
return await fn();
} catch (err) {
if (retries <= 0) throw err;
console.log(`还剩${retries}次重试机会`);
await new Promise(res => setTimeout(res, delay));
return retry(fn, retries - 1, delay);
}
}
// 使用示例
async function fetchData() {
return retry(() => fetch('https://unstable-api.com/data'));
}
- 并发控制
async function parallelWithLimit(tasks, limit = 5) {
const results = [];
const executing = [];
for (const task of tasks) {
const p = Promise.resolve().then(task);
results.push(p);
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
// 使用示例:批量处理100个URL,但每次最多并发5个
const urls = [...Array(100).keys()].map(i => `https://example.com/data/${i}`);
parallelWithLimit(
urls.map(url => () => fetch(url).then(r => r.json())),
5
).then(results => {
console.log('全部完成', results.length);
});
六、应用场景与选型建议
异步编程在哪些场景特别重要呢?
- 前端开发:AJAX请求、事件处理、动画效果
- Node.js后端:文件IO、数据库操作、网络请求
- WebSocket:实时通信应用
- 定时任务:延时执行、定期检查
技术选型建议:
- 简单异步:Promise足够
- 复杂异步逻辑:async/await
- 并行任务:Promise.all/Promise.race
- 需要取消的异步:考虑AbortController
七、常见坑与最佳实践
最后分享一些我在实践中总结的经验:
- 不要忘记错误处理:Promise要catch,async/await要try-catch
- 避免await滥用:无关的异步操作可以并行
- 注意内存泄漏:未完成的Promise会一直占用内存
- 性能优化:合理控制并发数
- 调试技巧:善用async stack traces
// 不好的写法:顺序await无关操作
async function slowExample() {
const user = await getUser(); // 等这个完成
const orders = await getOrders(); // 才开始这个
// ...
}
// 好的写法:并行执行
async function fastExample() {
const [user, orders] = await Promise.all([
getUser(),
getOrders()
]);
// ...
}
记住,异步编程不是洪水猛兽,只要掌握了正确的方法和工具,你就能写出既高效又易维护的代码。希望这篇文章能帮你解开JavaScript异步编程的谜团!
评论