JavaScript 异步编程完全指南
JavaScript 的异步编程是现代前端开发的核心内容。随着 Web 应用变得越来越复杂,处理异步操作(如网络请求、定时器、文件操作等)成为了每个开发者必须掌握的技能。本文将从零开始,带你深入理解 JavaScript 异步编程的各个方面,从基础的回调函数到最现代化的 async/await 语法。
一、同步 vs 异步
1.1 什么是同步编程
同步编程是按照代码顺序依次执行的,每一行代码都必须等待上一行代码执行完成:
console.log('开始执行');
const result = sum(1, 2);
console.log('结果:', result);
function sum(a, b) { setTimeout(() => { return a + b; }, 2000); }
console.log('执行结束');
|
执行顺序:
- 输出 “开始执行”
- 调用 sum 函数(但不会等待,直接执行下一行)
- 输出 “结果:”(但 result 还是 undefined)
- 输出 “执行结束”
- 2 秒后 sum 函数返回 3
1.2 什么是异步编程
异步编程允许代码在等待异步操作(如网络请求、定时器等)完成时继续执行,不会阻塞主线程:
console.log('开始执行');
setTimeout(() => { console.log('延迟执行'); }, 1000);
console.log('执行结束');
|
执行顺序:
- 输出 “开始执行”
- 设置定时器(1 秒后执行回调函数)
- 输出 “执行结束”
- 1 秒后输出 “延迟执行”
1.3 同步和异步的区别
| 特性 | 同步 | 异步 |
|---|
| 执行方式 | 顺序执行 | 并行执行 |
| 主线程阻塞 | 会阻塞 | 不阻塞 |
| 使用场景 | 文件操作 | 网络请求、定时器 |
二、回调函数
2.1 基础回调函数
回调函数是最基本的异步编程方式,将函数作为参数传递给另一个函数,在异步操作完成后调用:
function fetchData(callback) { setTimeout(() => { const data = { id: 1, name: '张三', age: 25, email: 'zhangsan@example.com' }; callback(data); }, 1000); }
fetchData((user) => { console.log('用户数据:', user); console.log('姓名:', user.name); console.log('年龄:', user.age); console.log('邮箱:', user.email); });
|
2.2 回调地狱
当多个异步操作有依赖关系时,会产生回调嵌套,这就是所谓的”回调地狱”:
getUser((user) => { console.log('用户:', user.name);
getUserPosts(user.id, (posts) => { console.log('用户的文章:', posts);
getPostComments(posts[0].id, (comments) => { console.log('文章的评论:', comments);
}); }); });
function getUser(callback) { setTimeout(() => { callback({ name: '张三' }); }, 1000); }
function getUserPosts(userId, callback) { setTimeout(() => { callback([ { id: 1, title: '文章1' }, { id: 2, title: '文章2' } ]); }, 1000); }
function getPostComments(postId, callback) { setTimeout(() => { callback([ { id: 1, content: '评论1' }, { id: 2, content: '评论2' } ]); }, 1000); }
|
回调地狱的问题:
- 代码难以阅读和维护
- 嵌套过深,容易出错
- 代码难以扩展
- 错误处理复杂
2.3 解决回调地狱的方法
1. 使用 Promise
getUser() .then(user => { console.log('用户:', user.name); return getUserPosts(user.id); }) .then(posts => { console.log('用户的文章:', posts); return getPostComments(posts[0].id); }) .then(comments => { console.log('文章的评论:', comments); }) .catch(error => { console.error('错误:', error); });
|
2. 使用 async/await
async function getUserData() { try { const user = await getUser(); console.log('用户:', user.name);
const posts = await getUserPosts(user.id); console.log('用户的文章:', posts);
const comments = await getPostComments(posts[0].id); console.log('文章的评论:', comments); } catch (error) { console.error('错误:', error); } }
getUserData();
|
三、Promise
3.1 Promise 的概念
Promise 是异步编程的一种解决方案,它是一个对象,代表一个异步操作的最终完成或失败。Promise 有三种状态:
- pending:进行中
- fulfilled(成功)
- rejected(失败)
Promise 状态一旦改变,就不会再改变。
3.2 创建 Promise
const promise = new Promise((resolve, reject) => { setTimeout(() => { const success = Math.random() > 0.3;
if (success) { resolve('操作成功'); } else { reject('操作失败'); } }, 1000); });
promise .then((data) => { console.log('成功:', data); }) .catch((error) => { console.error('失败:', error); }) .finally(() => { console.log('操作完成'); });
|
3.3 Promise 的三种状态转换
const fulfilled = new Promise((resolve) => { setTimeout(() => { resolve('操作成功'); }, 1000); });
const rejected = new Promise((_, reject) => { setTimeout(() => { reject(new Error('操作失败')); }, 1000); });
let state = 'pending'; let result;
const promise = new Promise((resolve, reject) => { if (state === 'pending') { resolve('成功'); } state = 'fulfilled'; result = '这行代码不会执行'; });
promise.then((data) => { console.log('数据:', data); console.log('结果:', result); });
|
3.4 Promise 链式调用
Promise 支持链式调用,每次 then 都返回一个新的 Promise:
Promise.resolve(1) .then((value) => { console.log('第一步:', value); return value * 2; }) .then((value) => { console.log('第二步:', value); return value + 3; }) .then((value) => { console.log('第三步:', value); }) .catch((error) => { console.error('错误:', error); }) .finally(() => { console.log('操作完成'); });
|
3.5 Promise.all 和 Promise.race
Promise.all
Promise.all 接收一个 Promise 数组,当所有 Promise 都成功时,才返回结果;如果有任何一个失败,立即返回失败结果:
const p1 = Promise.resolve(1); const p2 = Promise.resolve(2); const p3 = Promise.resolve(3); const p4 = Promise.reject('失败');
Promise.all([p1, p2, p3, p4]) .then((values) => { console.log('所有结果:', values); }) .catch((error) => { console.error('错误:', error); });
|
实际应用:并发请求多个 API
async function fetchAllData() { try { const [users, posts, comments] = await Promise.all([ fetch('/api/users').then(res => res.json()), fetch('/api/posts').then(res => res.json()), fetch('/api/comments').then(res => res.json()) ]);
console.log('用户:', users); console.log('文章:', posts); console.log('评论:', comments); } catch (error) { console.error('请求失败:', error); } }
|
Promise.race
Promise.race 接收一个 Promise 数组,返回最先完成(无论是成功还是失败)的那个 Promise 的结果:
const p1 = new Promise(resolve => setTimeout(() => resolve('p1'), 100)); const p2 = new Promise(resolve => setTimeout(() => resolve('p2'), 200)); const p3 = new Promise(resolve => setTimeout(() => resolve('p3'), 50));
Promise.race([p1, p2, p3]) .then((value) => { console.log('最先完成的:', value); });
|
实际应用:请求超时处理
function fetchWithTimeout(url, timeout = 5000) { return Promise.race([ fetch(url), new Promise((_, reject) => { setTimeout(() => { reject(new Error('请求超时')); }, timeout); }) ]); }
fetchWithTimeout('/api/data', 3000) .then(res => res.json()) .then(data => console.log('数据:', data)) .catch(error => console.error('错误:', error));
|
3.6 Promise.allSettled
Promise.allSettled 会等待所有 Promise 完成,无论成功或失败,返回一个数组:
const p1 = Promise.resolve(1); const p2 = Promise.reject('错误'); const p3 = Promise.resolve(3);
Promise.allSettled([p1, p2, p3]) .then((results) => { results.forEach((result, index) => { if (result.status === 'fulfilled') { console.log(`Promise ${index}: 成功`, result.value); } else { console.error(`Promise ${index}: 失败`, result.reason); } }); });
|
输出:
Promise 0: 成功 1 Promise 1: 失败 错误 Promise 2: 成功 3
|
四、async 和 await
4.1 async 函数
async 函数是一个返回 Promise 对象的函数:
async function getData() { return 'Hello World'; }
function getData() { return Promise.resolve('Hello World'); }
getData() .then((result) => { console.log(result); });
|
4.2 await 关键字
await 只能在 async 函数中使用,它会暂停异步函数的执行,等待 Promise 完成,然后返回结果:
async function fetchUser() { const response = await fetch('/api/user'); const user = await response.json();
return user; }
fetchUser() .then(user => console.log('用户:', user)) .catch(error => console.error('错误:', error));
|
4.3 实战案例
案例 1:用户登录流程
async function login(email, password) { try { const response = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email, password }) });
if (!response.ok) { throw new Error('登录失败'); }
const user = await response.json();
localStorage.setItem('token', user.token);
const userProfile = await fetchUserProfile(user.id);
return { success: true, user: userProfile }; } catch (error) { console.error('登录失败:', error); return { success: false, error: error.message }; } }
async function fetchUserProfile(userId) { const response = await fetch(`/api/users/${userId}`); return response.json(); }
login('zhangsan@example.com', 'password') .then(result => { if (result.success) { console.log('登录成功:', result.user); } else { console.error('登录失败:', result.error); } });
|
案例 2:并行请求
async function fetchHomePageData() { try { const [bannerData, navData, articles, hotTopics] = await Promise.all([ fetch('/api/banner').then(res => res.json()), fetch('/api/nav').then(res => res.json()), fetch('/api/articles').then(res => res.json()), fetch('/api/hot-topics').then(res => res.json()) ]);
return { banner: bannerData, nav: navData, articles: articles, hotTopics: hotTopics }; } catch (error) { console.error('获取首页数据失败:', error); return null; } }
fetchHomePageData() .then(data => { if (data) { console.log('Banner:', data.banner); console.log('导航:', data.nav); console.log('文章:', data.articles); console.log('热门话题:', data.hotTopics); } });
|
案例 3:表单提交
async function submitForm(formData) { try { const validation = validateForm(formData); if (!validation.valid) { return { success: false, errors: validation.errors }; }
const response = await fetch('/api/submit', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(formData) });
if (!response.ok) { throw new Error('提交失败'); }
const result = await response.json();
if (formData.file) { await uploadFile(formData.file, result.fileId); }
return { success: true, data: result }; } catch (error) { console.error('表单提交失败:', error); return { success: false, error: error.message }; } }
function validateForm(formData) { const errors = {};
if (!formData.username) { errors.username = '用户名不能为空'; }
if (!formData.email) { errors.email = '邮箱不能为空'; }
return { valid: Object.keys(errors).length === 0, errors }; }
async function uploadFile(file, fileId) { const formData = new FormData(); formData.append('file', file); formData.append('fileId', fileId);
await fetch('/api/upload', { method: 'POST', body: formData }); }
const form = { username: '张三', email: 'zhangsan@example.com', password: 'password123', file: document.getElementById('fileInput').files[0] };
submitForm(form) .then(result => { if (result.success) { console.log('提交成功:', result.data); } else { console.error('提交失败:', result.errors); } });
|
案例 4:轮播图
async function startCarousel(images, interval = 2000) { let currentIndex = 0; const slides = document.querySelectorAll('.slide');
function showSlide(index) { slides.forEach((slide, i) => { slide.classList.remove('active'); slide.style.opacity = i === index ? '1' : '0'; slide.style.transition = 'opacity 0.5s'; }); }
function nextSlide() { currentIndex = (currentIndex + 1) % images.length; showSlide(currentIndex); }
showSlide(currentIndex);
return new Promise((resolve) => { const timer = setInterval(nextSlide, interval);
function stop() { clearInterval(timer); resolve(); }
return { stop }; }); }
async function initCarousel() { const images = await fetch('/api/carousel').then(res => res.json());
const stop = await startCarousel(images);
setTimeout(() => { stop(); }, 10000); }
initCarousel();
|
五、错误处理策略
5.1 try-catch 块
使用 try-catch 捕获异步操作中的错误:
async function riskyOperation() { try { const result = await fetch('/api/risky'); return await result.json(); } catch (error) { console.error('操作失败:', error); throw error; } }
|
5.2 Promise.catch
fetch('/api/data') .then(response => response.json()) .then(data => { console.log('数据:', data); }) .catch(error => { console.error('获取数据失败:', error); });
|
5.3 错误边界
async function safeAsyncOperation(operation) { try { return await operation(); } catch (error) { console.error('操作失败:', error); return null; } }
async function getUser(userId) { return safeAsyncOperation(async () => { const response = await fetch(`/api/users/${userId}`); return await response.json(); }); }
const user = await getUser(1); if (user) { console.log('用户:', user.name); } else { console.log('用户不存在'); }
|
六、性能优化
6.1 并发控制
async function fetchInBatches(urls, batchSize = 3) { const results = [];
for (let i = 0; i < urls.length; i += batchSize) { const batch = urls.slice(i, i + batchSize);
const batchResults = await Promise.all( batch.map(url => fetch(url).then(res => res.json())) );
results.push(...batchResults); }
return results; }
const urls = [ 'https://api.example.com/1', 'https://api.example.com/2', 'https://api.example.com/3', 'https://api.example.com/4', 'https://api.example.com/5' ];
fetchInBatches(urls, 2) .then(results => { console.log('所有请求完成:', results); });
|
6.2 请求取消
class AbortController { constructor() { this.abortController = new AbortController(); }
get signal() { return this.abortController.signal; }
abort() { this.abortController.abort(); } }
async function fetchWithTimeout(url, timeout = 5000) { const controller = new AbortController(); const timeoutId = setTimeout(() => { controller.abort(); }, timeout);
try { const response = await fetch(url, { signal: controller.signal }); clearTimeout(timeoutId); return await response.json(); } catch (error) { if (error.name === 'AbortError') { console.log('请求超时'); } else { throw error; } } }
fetchWithTimeout('/api/data', 3000);
|
6.3 缓存策略
class Cache { constructor(ttl = 60000) { this.cache = new Map(); this.ttl = ttl; }
set(key, value) { const expiresAt = Date.now() + this.ttl; this.cache.set(key, { value, expiresAt }); }
get(key) { const cached = this.cache.get(key);
if (!cached) return null;
if (Date.now() > cached.expiresAt) { this.cache.delete(key); return null; }
return cached.value; }
clear() { this.cache.clear(); } }
const cache = new Cache(5000);
async function fetchWithCache(url) { const cached = cache.get(url); if (cached) { console.log('使用缓存'); return cached; }
const response = await fetch(url); const data = await response.json();
cache.set(url, data);
return data; }
fetchWithCache('https://api.example.com/data');
|
七、总结
JavaScript 异步编程的方式:
- 回调函数:基础但易导致回调地狱
- Promise:改进异步处理,支持链式调用
- async/await:最现代化的异步编程方式
最佳实践:
- 使用 try-catch 处理错误
- 使用 Promise.all 并发请求
- 合理使用缓存
- 控制请求频率(节流/防抖)
- 使用并发控制避免过多并发请求
掌握异步编程是现代前端开发的关键,希望本文能帮助你更好地理解和应用 JavaScript 异步编程!