在前端开发中,异步编程是不可或缺的技能。无论是从服务器获取数据、处理文件上传,还是实现动态交互,异步操作无处不在。然而,对于初级 JavaScript 开发者来说,回调函数、Promise 和 async/await 常常令人困惑。作为一名中级开发者,熟练掌握异步编程不仅能让代码更优雅,还能提升性能和用户体验。
关于作者:我是小贺,乐于分享各种前端知识,同时欢迎大家关注我的个人博客以及微信公众号[小贺前端笔记]
本文将带你从 Promise
的基础开始,逐步深入到 async/await
的高级用法,结合实战案例和优化技巧,帮助你从初级迈向中级。无论你是想优化 API 调用,还是解决回调地狱,这篇文章都将为你提供清晰的指引。让我们一起探索 JavaScript 异步编程的魅力!
JavaScript 是单线程语言,异步编程允许我们在等待耗时操作(如网络请求、定时器)时不阻塞主线程。常见的异步场景包括:
setTimeout
或 setInterval
。初级开发者可能用过回调函数处理异步:
function fetchData(callback) {
setTimeout(() => {
callback('Data received');
}, 1000);
}
fetchData(data => {
console.log(data); // Data received
});
问题:多层嵌套回调会导致“回调地狱”,代码难以维护:
fetchData(data1 => {
fetchData(data2 => {
fetchData(data3 => {
console.log(data1, data2, data3);
});
});
});
Promise
是一个表示异步操作最终结果的对象,有三种状态:pending
(进行中)、fulfilled
(成功)、rejected
(失败)。
基本用法:
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Success');
// reject(new Error('Failed'));
}, 1000);
});
promise
.then(result => console.log(result)) // Success
.catch(error => console.error(error));
链式调用:解决回调地狱:
fetchData()
.then(data1 => fetchData())
.then(data2 => fetchData())
.then(data3 => console.log(data1, data2, data3))
.catch(error => console.error(error));
技巧:总是用 .catch()
处理错误,避免未捕获的异常。
async/await
是 Promise
的语法糖,让异步代码看起来像同步代码,提升可读性。
async
:声明一个异步函数,返回一个 Promise
。await
:暂停异步函数执行,等待 Promise
解析,只能用于 async
函数内。示例:
async function fetchData() {
return new Promise(resolve => {
setTimeout(() => resolve('Data received'), 1000);
});
}
async function main() {
const data = await fetchData();
console.log(data); // Data received
}
main();
使用 try/catch
捕获错误,简洁优雅:
async function main() {
try {
const data = await fetchData();
console.log(data);
} catch (error) {
console.error('Error:', error);
}
}
技巧:相比 Promise
的 .catch
,try/catch
更符合同步代码的习惯。
当需要同时发起多个异步请求时,Promise.all
可以并行执行,提高效率。
示例:同时获取多个 API 数据:
async function fetchMultiple() {
const urls = [
'https://api.example.com/data1',
'https://api.example.com/data2'
];
const promises = urls.map(url => fetch(url).then(res => res.json()));
const results = await Promise.all(promises);
console.log(results); // [data1, data2]
}
注意:如果任一 Promise
失败,Promise.all
会立即抛出错误,需用 try/catch
捕获。
Promise.race
返回最先完成(成功或失败)的 Promise
结果,适合超时控制。
示例:设置请求超时:
async function fetchWithTimeout(url, timeout = 5000) {
const fetchPromise = fetch(url).then(res => res.json());
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Request timed out')), timeout);
});
return Promise.race([fetchPromise, timeoutPromise]);
}
让我们通过一个前端案例,综合运用 Promise
和 async/await
,实现从多个 API 获取用户数据并渲染到页面。
假设你正在开发一个用户管理应用,需要:
模拟 API:
function fetchUsers() {
return new Promise(resolve => {
setTimeout(() => resolve([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]), 1000);
});
}
function fetchRoles() {
return new Promise(resolve => {
setTimeout(() => resolve([
{ id: 1, role: 'Admin' },
{ id: 2, role: 'User' }
]), 1500);
});
}
async function loadUserList() {
const userList = document.getElementById('userList');
userList.innerHTML = 'Loading... ';
try {
// 并行获取数据
const [users, roles] = await Promise.all([fetchUsers(), fetchRoles()]);
// 合并数据
const userData = users.map(user => ({
...user,
role: roles.find(role => role.id === user.id)?.role || 'Unknown'
}));
// 渲染列表
userList.innerHTML = '';
userData.forEach(({ name, role }) => {
const li = document.createElement('li');
li.textContent = `${name} - ${role}`;
userList.appendChild(li);
});
} catch (error) {
userList.innerHTML = `Error: ${error.message}`;
}
}
loadUserList();
HTML 结构:
<ul id="userList">ul>
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const res = await fetch(url);
return await res.json();
} catch (error) {
if (i === retries - 1) throw error;
}
}
}
从 Promise
到 async/await
,再到 Promise.all
和 Promise.race
,异步编程让 JavaScript 开发者能够高效处理复杂场景。通过本文的实战案例,你学会了如何优雅地获取和渲染 API 数据,同时优化性能和错误处理。
进阶建议:
fetch
替代品(如 axios
)以简化请求处理。欢迎在评论区分享你的异步编程经验,或提问你的疑惑!让我们一起成为更优秀的前端开发者!