JavaScript 异步编程:从回调到 Promise 再到各种骚操作
这一篇聊聊 JavaScript 的异步编程:从最基础的回调函数,到 Promise,再到 async/await,
最后再讲一些实际开发中常用的模式和"骚操作"。
为什么需要异步?
JavaScript 是单线程的,如果所有操作都是同步的,遇到耗时操作(网络请求、文件读取、定时器等)时,整个程序就会阻塞,用户界面会卡住。
异步编程让这些耗时操作在后台执行,不阻塞主线程,等操作完成后再通过回调、Promise 或 async/await 来处理结果。
回调函数:最原始的异步方式
在 Promise 和 async/await 出现之前,JavaScript 的异步主要靠回调函数:
1// 典型的回调模式
2setTimeout(() => {
3 console.log('1秒后执行');
4}, 1000);
5
6// Node.js 文件读取
7fs.readFile('file.txt', 'utf8', (err, data) => {
8 if (err) {
9 console.error('读取失败:', err);
10 return;
11 }
12 console.log('文件内容:', data);
13});
回调函数的问题:
- 回调地狱(Callback Hell):多层嵌套的回调让代码难以阅读和维护。
- 错误处理分散:每个回调都要单独处理错误。
- 难以组合:多个异步操作串行或并行执行时,代码会变得非常复杂。
1// 回调地狱示例
2fs.readFile('file1.txt', 'utf8', (err1, data1) => {
3 if (err1) {
4 console.error(err1);
5 return;
6 }
7 fs.readFile('file2.txt', 'utf8', (err2, data2) => {
8 if (err2) {
9 console.error(err2);
10 return;
11 }
12 fs.writeFile('output.txt', data1 + data2, (err3) => {
13 if (err3) {
14 console.error(err3);
15 return;
16 }
17 console.log('完成');
18 });
19 });
20});
Promise:异步编程的基础
Promise 是 ES6 引入的异步编程解决方案,它代表一个异步操作的最终完成或失败。
Promise 基础
1// 创建一个 Promise
2const promise = new Promise((resolve, reject) => {
3 // 异步操作
4 setTimeout(() => {
5 const success = Math.random() > 0.5;
6 if (success) {
7 resolve('操作成功');
8 } else {
9 reject(new Error('操作失败'));
10 }
11 }, 1000);
12});
13
14// 使用 Promise
15promise
16 .then(result => {
17 console.log(result);
18 })
19 .catch(error => {
20 console.error(error);
21 });
Promise 的三种状态
- pending(等待中):初始状态,既不是成功也不是失败。
- fulfilled(已成功):操作成功完成。
- rejected(已失败):操作失败。
Promise 的状态一旦改变就不会再变,只能从 pending 变为 fulfilled 或 rejected。
Promise 链式调用
Promise 的 then 方法返回一个新的 Promise,可以链式调用:
1fetch('/api/user')
2 .then(response => response.json())
3 .then(user => {
4 console.log('用户信息:', user);
5 return fetch(`/api/posts?userId=${user.id}`);
6 })
7 .then(response => response.json())
8 .then(posts => {
9 console.log('用户文章:', posts);
10 })
11 .catch(error => {
12 console.error('错误:', error);
13 });
Promise 的常用方法
Promise.all:等待所有 Promise 完成
1const promise1 = fetch('/api/data1');
2const promise2 = fetch('/api/data2');
3const promise3 = fetch('/api/data3');
4
5Promise.all([promise1, promise2, promise3])
6 .then(responses => {
7 // 所有请求都成功
8 return Promise.all(responses.map(r => r.json()));
9 })
10 .then(results => {
11 console.log('所有数据:', results);
12 })
13 .catch(error => {
14 // 任何一个请求失败,整个 Promise.all 就会失败
15 console.error('有请求失败:', error);
16 });
Promise.allSettled:等待所有 Promise 完成(无论成功或失败)
1Promise.allSettled([promise1, promise2, promise3])
2 .then(results => {
3 results.forEach((result, index) => {
4 if (result.status === 'fulfilled') {
5 console.log(`请求 ${index + 1} 成功:`, result.value);
6 } else {
7 console.log(`请求 ${index + 1} 失败:`, result.reason);
8 }
9 });
10 });
Promise.race:返回第一个完成的 Promise
1// 超时控制
2const fetchWithTimeout = (url, timeout = 5000) => {
3 return Promise.race([
4 fetch(url),
5 new Promise((_, reject) =>
6 setTimeout(() => reject(new Error('请求超时')), timeout)
7 )
8 ]);
9};
10
11fetchWithTimeout('/api/data', 3000)
12 .then(response => response.json())
13 .then(data => console.log(data))
14 .catch(error => console.error(error));
Promise.any:返回第一个成功的 Promise
1// 尝试多个数据源,返回第一个成功的
2const sources = [
3 fetch('/api/primary'),
4 fetch('/api/backup1'),
5 fetch('/api/backup2')
6];
7
8Promise.any(sources)
9 .then(response => response.json())
10 .then(data => {
11 console.log('获取到数据:', data);
12 })
13 .catch(error => {
14 // 所有请求都失败
15 console.error('所有数据源都失败:', error);
16 });
async/await:让异步代码看起来像同步代码
async/await 是 ES2017 引入的语法糖,基于 Promise,让异步代码的写法更接近同步代码。
基础用法
1// async 函数总是返回一个 Promise
2async function fetchUser(id) {
3 const response = await fetch(`/api/user/${id}`);
4 const user = await response.json();
5 return user;
6}
7
8// 使用
9fetchUser(1)
10 .then(user => console.log(user))
11 .catch(error => console.error(error));
错误处理
1// 使用 try/catch
2async function fetchUser(id) {
3 try {
4 const response = await fetch(`/api/user/${id}`);
5 if (!response.ok) {
6 throw new Error(`HTTP error! status: ${response.status}`);
7 }
8 const user = await response.json();
9 return user;
10 } catch (error) {
11 console.error('获取用户失败:', error);
12 throw error; // 重新抛出错误
13 }
14}
并行执行
1// 错误:串行执行(慢)
2async function fetchAllUsers() {
3 const user1 = await fetch('/api/user/1');
4 const user2 = await fetch('/api/user/2');
5 const user3 = await fetch('/api/user/3');
6 return [user1, user2, user3];
7}
8
9// 正确:并行执行(快)
10async function fetchAllUsers() {
11 const [user1, user2, user3] = await Promise.all([
12 fetch('/api/user/1'),
13 fetch('/api/user/2'),
14 fetch('/api/user/3')
15 ]);
16 return [user1, user2, user3];
17}
常用操作和模式
1. 重试机制
1async function fetchWithRetry(url, maxRetries = 3) {
2 for (let i = 0; i < maxRetries; i++) {
3 try {
4 const response = await fetch(url);
5 if (response.ok) {
6 return await response.json();
7 }
8 throw new Error(`HTTP ${response.status}`);
9 } catch (error) {
10 if (i === maxRetries - 1) throw error;
11 console.log(`重试 ${i + 1}/${maxRetries}...`);
12 await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
13 }
14 }
15}
2. 并发控制:限制同时执行的异步操作数量
1async function limitConcurrency(tasks, limit) {
2 const results = [];
3 const executing = [];
4
5 for (const task of tasks) {
6 const promise = task().then(result => {
7 executing.splice(executing.indexOf(promise), 1);
8 return result;
9 });
10
11 results.push(promise);
12 executing.push(promise);
13
14 if (executing.length >= limit) {
15 await Promise.race(executing);
16 }
17 }
18
19 return Promise.all(results);
20}
21
22// 使用
23const tasks = Array.from({ length: 10 }, (_, i) =>
24 () => fetch(`/api/data/${i}`)
25);
26
27limitConcurrency(tasks, 3).then(results => {
28 console.log('完成:', results);
29});
3. 防抖和节流
1// 防抖:延迟执行,如果再次触发则重新计时
2function debounce(func, delay) {
3 let timeoutId;
4 return function (...args) {
5 clearTimeout(timeoutId);
6 timeoutId = setTimeout(() => func.apply(this, args), delay);
7 };
8}
9
10// 节流:限制执行频率
11function throttle(func, delay) {
12 let lastCall = 0;
13 return function (...args) {
14 const now = Date.now();
15 if (now - lastCall >= delay) {
16 lastCall = now;
17 func.apply(this, args);
18 }
19 };
20}
21
22// 异步版本的防抖
23function asyncDebounce(func, delay) {
24 let timeoutId;
25 let lastPromise;
26 return function (...args) {
27 return new Promise((resolve, reject) => {
28 clearTimeout(timeoutId);
29 timeoutId = setTimeout(async () => {
30 try {
31 const result = await func.apply(this, args);
32 resolve(result);
33 } catch (error) {
34 reject(error);
35 }
36 }, delay);
37 });
38 };
39}
4. 超时包装器
1function withTimeout(promise, timeout, errorMessage = '操作超时') {
2 return Promise.race([
3 promise,
4 new Promise((_, reject) =>
5 setTimeout(() => reject(new Error(errorMessage)), timeout)
6 )
7 ]);
8}
9
10// 使用
11const data = await withTimeout(
12 fetch('/api/slow-endpoint'),
13 5000,
14 '请求超时,请稍后重试'
15);
5. 取消 Promise
1function cancellablePromise(executor) {
2 let cancel;
3 const promise = new Promise((resolve, reject) => {
4 cancel = () => {
5 reject(new Error('操作已取消'));
6 };
7 executor(resolve, reject);
8 });
9 return { promise, cancel };
10}
11
12// 使用
13const { promise, cancel } = cancellablePromise((resolve, reject) => {
14 setTimeout(() => resolve('完成'), 5000);
15});
16
17// 3秒后取消
18setTimeout(() => cancel(), 3000);
19
20promise
21 .then(result => console.log(result))
22 .catch(error => console.error(error)); // "操作已取消"
6. 批量处理:分批执行异步操作
1async function batchProcess(items, processor, batchSize = 5) {
2 const results = [];
3 for (let i = 0; i < items.length; i += batchSize) {
4 const batch = items.slice(i, i + batchSize);
5 const batchResults = await Promise.all(
6 batch.map(item => processor(item))
7 );
8 results.push(...batchResults);
9 }
10 return results;
11}
12
13// 使用
14const items = Array.from({ length: 100 }, (_, i) => i);
15const results = await batchProcess(
16 items,
17 async (item) => {
18 // 处理每个项目
19 return await processItem(item);
20 },
21 10 // 每批处理10个
22);
7. 顺序执行:一个接一个处理
1async function sequentialProcess(items, processor) {
2 const results = [];
3 for (const item of items) {
4 const result = await processor(item);
5 results.push(result);
6 }
7 return results;
8}
9
10// 使用
11const items = [1, 2, 3, 4, 5];
12const results = await sequentialProcess(
13 items,
14 async (item) => {
15 console.log(`处理 ${item}...`);
16 return await processItem(item);
17 }
18);
8. 条件执行:根据条件决定是否执行异步操作
1async function conditionalExecute(condition, asyncFn) {
2 if (await condition()) {
3 return await asyncFn();
4 }
5 return null;
6}
7
8// 使用
9const result = await conditionalExecute(
10 async () => {
11 const cache = await getCache();
12 return !cache; // 缓存不存在时才执行
13 },
14 async () => {
15 return await fetchData();
16 }
17);
9. 缓存 Promise:避免重复请求
1function cachePromise(fn) {
2 const cache = new Map();
3 return function (...args) {
4 const key = JSON.stringify(args);
5 if (cache.has(key)) {
6 return cache.get(key);
7 }
8 const promise = fn.apply(this, args).finally(() => {
9 // 可选:设置缓存过期时间
10 // setTimeout(() => cache.delete(key), 60000);
11 });
12 cache.set(key, promise);
13 return promise;
14 };
15}
16
17// 使用
18const cachedFetch = cachePromise(fetch);
19
20// 多次调用相同的 URL,只会发起一次请求
21cachedFetch('/api/data');
22cachedFetch('/api/data'); // 使用缓存的 Promise
10. Promise 队列:按顺序执行异步任务
1class PromiseQueue {
2 constructor(concurrency = 1) {
3 this.concurrency = concurrency;
4 this.running = 0;
5 this.queue = [];
6 }
7
8 add(task) {
9 return new Promise((resolve, reject) => {
10 this.queue.push({
11 task,
12 resolve,
13 reject
14 });
15 this.process();
16 });
17 }
18
19 async process() {
20 if (this.running >= this.concurrency || this.queue.length === 0) {
21 return;
22 }
23
24 this.running++;
25 const { task, resolve, reject } = this.queue.shift();
26
27 try {
28 const result = await task();
29 resolve(result);
30 } catch (error) {
31 reject(error);
32 } finally {
33 this.running--;
34 this.process();
35 }
36 }
37}
38
39// 使用
40const queue = new PromiseQueue(2); // 最多同时执行2个任务
41
42queue.add(() => fetch('/api/1'));
43queue.add(() => fetch('/api/2'));
44queue.add(() => fetch('/api/3')); // 等待前面的任务完成
11. 异步迭代器:处理流式数据
1async function* asyncGenerator() {
2 for (let i = 0; i < 5; i++) {
3 await new Promise(resolve => setTimeout(resolve, 1000));
4 yield i;
5 }
6}
7
8// 使用 for await...of
9(async () => {
10 for await (const value of asyncGenerator()) {
11 console.log(value);
12 }
13})();
12. 错误重试的指数退避
1async function fetchWithExponentialBackoff(url, maxRetries = 5) {
2 for (let i = 0; i < maxRetries; i++) {
3 try {
4 const response = await fetch(url);
5 if (response.ok) {
6 return await response.json();
7 }
8 throw new Error(`HTTP ${response.status}`);
9 } catch (error) {
10 if (i === maxRetries - 1) throw error;
11
12 // 指数退避:1s, 2s, 4s, 8s, 16s
13 const delay = Math.pow(2, i) * 1000;
14 console.log(`重试 ${i + 1}/${maxRetries},等待 ${delay}ms...`);
15 await new Promise(resolve => setTimeout(resolve, delay));
16 }
17 }
18}
实际开发中的最佳实践
1. 总是处理错误
1// 不好的做法
2async function fetchData() {
3 const data = await fetch('/api/data');
4 return data.json();
5}
6
7// 好的做法
8async function fetchData() {
9 try {
10 const response = await fetch('/api/data');
11 if (!response.ok) {
12 throw new Error(`HTTP error! status: ${response.status}`);
13 }
14 return await response.json();
15 } catch (error) {
16 console.error('获取数据失败:', error);
17 throw error; // 或者返回默认值
18 }
19}
2. 避免在循环中使用 await
1// 不好的做法:串行执行
2for (const item of items) {
3 await processItem(item);
4}
5
6// 好的做法:并行执行
7await Promise.all(items.map(item => processItem(item)));
8
9// 或者需要限制并发时
10await limitConcurrency(
11 items.map(item => () => processItem(item)),
12 5
13);
3. 使用 Promise.allSettled 处理部分失败
1// 当某些操作失败不应该影响其他操作时
2const results = await Promise.allSettled([
3 fetch('/api/data1'),
4 fetch('/api/data2'),
5 fetch('/api/data3')
6]);
7
8const successful = results
9 .filter(r => r.status === 'fulfilled')
10 .map(r => r.value);
11
12const failed = results
13 .filter(r => r.status === 'rejected')
14 .map(r => r.reason);
4. 合理使用 async/await 和 Promise 方法
1// 简单的情况用 async/await
2async function getUser(id) {
3 const response = await fetch(`/api/user/${id}`);
4 return response.json();
5}
6
7// 需要并行执行时用 Promise.all
8const [user, posts] = await Promise.all([
9 fetch('/api/user/1').then(r => r.json()),
10 fetch('/api/posts').then(r => r.json())
11]);
12
13// 需要第一个结果时用 Promise.race
14const firstResult = await Promise.race([
15 fetch('/api/source1'),
16 fetch('/api/source2')
17]);
小结
JavaScript 的异步编程从回调函数到 Promise,再到 async/await,不断演进:
- 回调函数:最原始的方式,容易产生回调地狱。
- Promise:提供了链式调用和统一的错误处理,是异步编程的基础。
- async/await:让异步代码看起来像同步代码,更易读易写。
在实际开发中,掌握这些常用操作和模式:
- 并发控制、重试机制、超时处理
- 防抖节流、批量处理、顺序执行
- Promise 队列、缓存、取消机制
- 错误处理和最佳实践
理解这些模式,可以写出更健壮、更高效的异步代码。