JavaScript异步编程
JavaScript是一种单线程语言,但它能够处理异步操作,这使得它在处理网络请求、文件操作和定时任务等方面非常强大。本文将深入探讨JavaScript中的异步编程模式和技术。
异步编程基础
同步与异步
在理解异步编程之前,我们需要先了解同步和异步的区别:
- 同步执行:代码按照顺序一步一步执行,每一步都必须等待前一步完成。
- 异步执行:代码不必等待耗时操作完成,而是继续执行后续代码,当耗时操作完成时,通过回调、Promise等机制处理结果。
// 同步执行示例
console.log("步骤 1");
console.log("步骤 2");
console.log("步骤 3");
// 输出顺序:步骤 1, 步骤 2, 步骤 3
// 异步执行示例
console.log("步骤 1");
setTimeout(() => {
console.log("步骤 2");
}, 1000);
console.log("步骤 3");
// 输出顺序:步骤 1, 步骤 3, 步骤 2
JavaScript的事件循环
JavaScript的异步操作依赖于事件循环(Event Loop)机制:
- 调用栈(Call Stack):执行代码的地方,函数调用形成一个栈结构。
- 任务队列(Task Queue):存放异步任务的回调函数。
- 微任务队列(Microtask Queue):存放优先级更高的异步任务(如Promise回调)。
- 事件循环:不断检查调用栈是否为空,如果为空则依次执行微任务队列和任务队列中的任务。
console.log("开始");
setTimeout(() => {
console.log("定时器回调");
}, 0);
Promise.resolve().then(() => {
console.log("Promise回调");
});
console.log("结束");
// 输出顺序:开始, 结束, Promise回调, 定时器回调
回调函数
回调函数是最早用于处理异步操作的方式,它是一个作为参数传递给另一个函数的函数,在适当的时候被调用。
基本用法
function fetchData(callback) {
// 模拟网络请求
setTimeout(() => {
const data = { name: "张三", age: 30 };
callback(data);
}, 1000);
}
fetchData((data) => {
console.log("获取到的数据:", data);
});
console.log("请求已发送");
// 输出顺序:请求已发送, 获取到的数据: { name: '张三', age: 30 }
回调地狱
当多个异步操作需要依次执行时,回调函数会导致代码嵌套过深,形成"回调地狱":
fetchUserData(userId, (user) => {
fetchUserPosts(user.id, (posts) => {
fetchPostComments(posts[0].id, (comments) => {
fetchCommentAuthor(comments[0].authorId, (author) => {
console.log("作者信息:", author);
// 更多嵌套...
});
});
});
});
错误处理
回调函数的错误处理通常通过传递错误参数实现:
function fetchData(callback) {
// 模拟网络请求
setTimeout(() => {
const success = Math.random() > 0.5;
if (success) {
const data = { name: "张三", age: 30 };
callback(null, data);
} else {
callback(new Error("获取数据失败"), null);
}
}, 1000);
}
fetchData((error, data) => {
if (error) {
console.error("错误:", error.message);
return;
}
console.log("获取到的数据:", data);
});
Promise
Promise是ES6引入的一种处理异步操作的对象,它代表一个异步操作的最终完成(或失败)及其结果值。
基本用法
function fetchData() {
return new Promise((resolve, reject) => {
// 模拟网络请求
setTimeout(() => {
const success = Math.random() > 0.5;
if (success) {
const data = { name: "张三", age: 30 };
resolve(data);
} else {
reject(new Error("获取数据失败"));
}
}, 1000);
});
}
fetchData()
.then(data => {
console.log("获取到的数据:", data);
})
.catch(error => {
console.error("错误:", error.message);
})
.finally(() => {
console.log("请求结束");
});
Promise链式调用
Promise可以链式调用,解决回调地狱问题:
fetchUserData(userId)
.then(user => {
return fetchUserPosts(user.id);
})
.then(posts => {
return fetchPostComments(posts[0].id);
})
.then(comments => {
return fetchCommentAuthor(comments[0].authorId);
})
.then(author => {
console.log("作者信息:", author);
})
.catch(error => {
console.error("错误:", error.message);
});
Promise.all
Promise.all
接收一个Promise数组,当所有Promise都成功时返回所有结果的数组,如果有任何一个Promise失败,则立即返回失败:
const promise1 = fetchUserData(1);
const promise2 = fetchUserData(2);
const promise3 = fetchUserData(3);
Promise.all([promise1, promise2, promise3])
.then(results => {
const [user1, user2, user3] = results;
console.log("所有用户数据:", user1, user2, user3);
})
.catch(error => {
console.error("获取用户数据失败:", error.message);
});
Promise.race
Promise.race
接收一个Promise数组,返回最先完成的Promise的结果,无论成功或失败:
const promise1 = fetchData("endpoint1"); // 可能需要2秒
const promise2 = fetchData("endpoint2"); // 可能需要1秒
Promise.race([promise1, promise2])
.then(result => {
console.log("最快返回的数据:", result);
})
.catch(error => {
console.error("最快返回的错误:", error.message);
});
Promise.allSettled
Promise.allSettled
(ES2020引入)接收一个Promise数组,等待所有Promise完成(无论成功或失败),并返回它们的结果:
const promises = [
fetchUserData(1),
fetchUserData(2),
fetchUserData(999) // 假设这个ID不存在
];
Promise.allSettled(promises)
.then(results => {
results.forEach((result, index) => {
if (result.status === "fulfilled") {
console.log(`用户${index + 1}数据:`, result.value);
} else {
console.log(`用户${index + 1}数据获取失败:`, result.reason);
}
});
});
Promise.any
Promise.any
(ES2021引入)接收一个Promise数组,返回第一个成功的Promise的结果,如果所有Promise都失败,则返回一个包含所有失败原因的AggregateError:
const promises = [
fetchData("endpoint1"),
fetchData("endpoint2"),
fetchData("endpoint3")
];
Promise.any(promises)
.then(result => {
console.log("第一个成功的结果:", result);
})
.catch(error => {
console.log("所有Promise都失败了");
console.log("失败原因:", error.errors);
});
async/await
async/await
是ES2017引入的语法糖,基于Promise,使异步代码看起来更像同步代码,提高了可读性。
基本用法
async function fetchUserData(userId) {
try {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
const author = await fetchAuthor(comments[0].authorId);
console.log("作者信息:", author);
return author;
} catch (error) {
console.error("获取数据失败:", error.message);
throw error;
}
}
// 调用异步函数
fetchUserData(1)
.then(author => {
console.log("处理作者数据");
})
.catch(error => {
console.error("外部错误处理:", error.message);
});
并行执行
使用Promise.all
配合await
可以并行执行多个异步操作:
async function fetchAllData() {
try {
const [users, posts, comments] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchComments()
]);
console.log("用户数:", users.length);
console.log("文章数:", posts.length);
console.log("评论数:", comments.length);
return { users, posts, comments };
} catch (error) {
console.error("获取数据失败:", error.message);
throw error;
}
}
循环中的异步操作
在循环中使用async/await
需要注意是串行执行还是并行执行:
// 串行执行(一个接一个)
async function fetchUsersSequentially(userIds) {
const users = [];
for (const id of userIds) {
const user = await fetchUser(id); // 等待每个请求完成
users.push(user);
}
return users;
}
// 并行执行(同时发起所有请求)
async function fetchUsersParallel(userIds) {
const promises = userIds.map(id => fetchUser(id));
const users = await Promise.all(promises);
return users;
}
错误处理
async/await
中的错误处理可以使用try/catch:
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error("获取数据失败:", error.message);
// 可以选择重新抛出错误或返回默认值
return { error: true, message: error.message };
}
}
生成器函数与异步迭代
生成器函数(Generator Functions)可以暂停和恢复执行,使其成为处理异步操作的另一种方式。
基本用法
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 }
使用生成器处理异步操作
function fetchData(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`来自 ${url} 的数据`);
}, 1000);
});
}
function* fetchMultipleData() {
try {
const data1 = yield fetchData('url1');
console.log(data1);
const data2 = yield fetchData('url2');
console.log(data2);
return "所有数据获取完成";
} catch (error) {
console.error("错误:", error);
}
}
// 手动执行生成器
function runGenerator(gen) {
const iterator = gen();
function run(arg) {
const result = iterator.next(arg);
if (result.done) {
return result.value;
}
return Promise.resolve(result.value).then(
value => run(value),
error => {
iterator.throw(error);
return run();
}
);
}
return run();
}
runGenerator(fetchMultipleData)
.then(result => console.log(result))
.catch(error => console.error(error));
异步迭代器
ES2018引入了异步迭代器和for await...of
语法,简化了对异步数据源的迭代:
async function* asyncGenerator() {
yield await fetchData('url1');
yield await fetchData('url2');
yield await fetchData('url3');
}
async function processData() {
for await (const data of asyncGenerator()) {
console.log("处理数据:", data);
}
}
processData();
实际应用示例
使用Fetch API获取数据
// 使用Promise
function fetchUserData(userId) {
return fetch(`https://api.example.com/users/${userId}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
return response.json();
});
}
// 使用async/await
async function fetchUserData(userId) {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
return response.json();
}
实现超时处理
function fetchWithTimeout(url, timeout = 5000) {
return Promise.race([
fetch(url),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('请求超时')), timeout)
)
]);
}
// 使用
fetchWithTimeout('https://api.example.com/data', 3000)
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error.message));
重试机制
async function fetchWithRetry(url, retries = 3, delay = 1000) {
let lastError;
for (let i = 0; i < retries; i++) {
try {
return await fetch(url);
} catch (error) {
console.log(`尝试 ${i + 1} 失败,${i < retries - 1 ? '重试中...' : '放弃'}`);
lastError = error;
if (i < retries - 1) {
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
并发控制
async function fetchWithConcurrencyLimit(urls, limit = 3) {
const results = [];
const inProgress = new Set();
async function doFetch(url) {
try {
const response = await fetch(url);
const data = await response.json();
return data;
} finally {
inProgress.delete(url);
}
}
for (const url of urls) {
// 等待,直到并发数量低于限制
while (inProgress.size >= limit) {
await Promise.race(inProgress);
}
// 添加到进行中的请求
const promise = doFetch(url);
inProgress.add(promise);
// 存储结果(保持顺序)
results.push(await promise);
}
return results;
}
异步编程最佳实践
错误处理
始终处理异步操作中的错误:
// Promise
fetchData()
.then(data => {
// 处理数据
})
.catch(error => {
// 处理错误
console.error("错误:", error);
});
// async/await
async function fetchData() {
try {
const data = await fetchSomething();
// 处理数据
} catch (error) {
// 处理错误
console.error("错误:", error);
}
}
避免回调地狱
使用Promise或async/await替代嵌套回调:
// 不推荐
doSomething(function(result1) {
doSomethingElse(result1, function(result2) {
doAnotherThing(result2, function(result3) {
console.log(result3);
});
});
});
// 推荐 (Promise)
doSomething()
.then(result1 => doSomethingElse(result1))
.then(result2 => doAnotherThing(result2))
.then(result3 => {
console.log(result3);
})
.catch(error => {
console.error(error);
});
// 推荐 (async/await)
async function doSequentialWork() {
try {
const result1 = await doSomething();
const result2 = await doSomethingElse(result1);
const result3 = await doAnotherThing(result2);
console.log(result3);
} catch (error) {
console.error(error);
}
}
并行执行
当多个异步操作相互独立时,应该并行执行它们:
// 串行(慢)
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 };
}
取消异步操作
使用AbortController取消fetch请求:
function fetchWithCancel(url) {
const controller = new AbortController();
const signal = controller.signal;
const promise = fetch(url, { signal })
.then(response => response.json());
return {
promise,
cancel: () => controller.abort()
};
}
// 使用
const { promise, cancel } = fetchWithCancel('https://api.example.com/data');
// 5秒后取消请求
setTimeout(() => {
console.log('取消请求');
cancel();
}, 5000);
promise
.then(data => console.log('数据:', data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('请求已取消');
} else {
console.error('错误:', error);
}
});
异步函数组合
创建可组合的异步函数:
// 异步函数组合
const composeAsync = (...fns) => input =>
fns.reduce(
(chain, fn) => chain.then(fn),
Promise.resolve(input)
);
// 使用
const getUserData = composeAsync(
fetchUser,
user => fetchPosts(user.id),
posts => fetchComments(posts[0].id)
);
getUserData(1)
.then(comments => console.log('评论:', comments))
.catch(error => console.error('错误:', error));
总结
JavaScript的异步编程已经从回调函数发展到Promise,再到async/await,使得处理异步操作变得越来越简洁和直观。掌握这些异步编程技术对于开发高性能、响应式的JavaScript应用至关重要。
- 回调函数是最基本的异步处理方式,但容易导致回调地狱。
- Promise提供了更好的链式调用和错误处理机制。
- async/await让异步代码看起来更像同步代码,提高了可读性。
- 生成器和异步迭代器为处理异步数据流提供了强大工具。
在实际应用中,应根据具体需求选择合适的异步编程模式,并遵循最佳实践,如正确处理错误、避免回调地狱、并行执行独立操作等。