JavaScript异步编程的常见模式与技巧
异步编程基础
在 JavaScript 中,异步编程是处理非阻塞 I/O 操作、网络请求、定时器等场景的关键技术。JavaScript 是单线程语言,这意味着它在同一时间只能执行一个任务。然而,许多操作,如网络请求或读取文件,可能需要花费很长时间才能完成。如果这些操作是同步的,它们会阻塞主线程,导致用户界面冻结,直到操作完成。为了解决这个问题,JavaScript 提供了异步编程机制。
回调函数
回调函数是 JavaScript 中最基本的异步编程模式。当一个异步操作完成时,它会调用一个作为参数传递给它的函数,这个函数就是回调函数。
// 模拟一个异步操作,例如读取文件
function readFileAsync(filePath, callback) {
setTimeout(() => {
const data = '模拟文件内容';
callback(null, data);
}, 1000);
}
readFileAsync('example.txt', (error, data) => {
if (error) {
console.error('读取文件出错:', error);
} else {
console.log('文件内容:', data);
}
});
在这个例子中,readFileAsync
函数模拟了一个异步读取文件的操作。它接受一个文件路径和一个回调函数作为参数。setTimeout
模拟了异步操作的延迟,在延迟结束后,它调用回调函数,并传递可能的错误和数据。
然而,回调函数在处理多个异步操作时可能会导致回调地狱(Callback Hell),即回调函数嵌套过深,代码可读性和维护性变差。
// 回调地狱示例
function step1(callback) {
setTimeout(() => {
console.log('步骤1完成');
callback();
}, 1000);
}
function step2(callback) {
setTimeout(() => {
console.log('步骤2完成');
callback();
}, 1000);
}
function step3(callback) {
setTimeout(() => {
console.log('步骤3完成');
callback();
}, 1000);
}
step1(() => {
step2(() => {
step3(() => {
console.log('所有步骤完成');
});
});
});
Promise
Promise 是一种更优雅的处理异步操作的方式。它代表一个异步操作的最终完成(或失败)及其结果值。
// 使用 Promise 模拟读取文件
function readFileAsyncWithPromise(filePath) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
const data = '模拟文件内容';
resolve(data);
} else {
const error = new Error('读取文件出错');
reject(error);
}
}, 1000);
});
}
readFileAsyncWithPromise('example.txt')
.then(data => {
console.log('文件内容:', data);
})
.catch(error => {
console.log('读取文件出错:', error);
});
Promise 有三种状态:pending
(进行中)、fulfilled
(已成功)和 rejected
(已失败)。当 Promise 被 resolve
时,状态变为 fulfilled
,并调用 then
方法中的成功回调;当 Promise 被 reject
时,状态变为 rejected
,并调用 catch
方法中的错误回调。
通过链式调用 then
方法,可以处理多个异步操作,避免了回调地狱。
function step1() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('步骤1完成');
resolve();
}, 1000);
});
}
function step2() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('步骤2完成');
resolve();
}, 1000);
});
}
function step3() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('步骤3完成');
resolve();
}, 1000);
});
}
step1()
.then(() => step2())
.then(() => step3())
.then(() => {
console.log('所有步骤完成');
});
async/await
async/await
是基于 Promise 的一种更简洁的异步编程语法糖。async
函数总是返回一个 Promise,而 await
只能在 async
函数内部使用,它暂停 async
函数的执行,直到 Promise 被解决(resolved 或 rejected)。
// 使用 async/await 模拟读取文件
async function readFileAsyncWithAsyncAwait(filePath) {
try {
const data = await new Promise((resolve) => {
setTimeout(() => {
const success = true;
if (success) {
const data = '模拟文件内容';
resolve(data);
}
}, 1000);
});
console.log('文件内容:', data);
} catch (error) {
console.log('读取文件出错:', error);
}
}
readFileAsyncWithAsyncAwait('example.txt');
在这个例子中,await
暂停了 readFileAsyncWithAsyncAwait
函数的执行,直到 Promise 被解决。try/catch
块用于捕获可能的错误,这种方式使得异步代码看起来更像同步代码,大大提高了代码的可读性。
同样,使用 async/await
处理多个异步操作也非常简洁。
async function runSteps() {
await step1();
await step2();
await step3();
console.log('所有步骤完成');
}
async function step1() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('步骤1完成');
resolve();
}, 1000);
});
}
async function step2() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('步骤2完成');
resolve();
}, 1000);
});
}
async function step3() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('步骤3完成');
resolve();
}, 1000);
});
}
runSteps();
异步控制流模式
串行执行
串行执行意味着一个异步操作完成后再开始下一个异步操作。在使用 Promise 时,可以通过链式调用 then
方法来实现串行执行。
function task1() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('任务1完成');
resolve();
}, 1000);
});
}
function task2() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('任务2完成');
resolve();
}, 1000);
});
}
function task3() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('任务3完成');
resolve();
}, 1000);
});
}
task1()
.then(() => task2())
.then(() => task3())
.then(() => {
console.log('所有任务串行完成');
});
使用 async/await
时,代码更加直观。
async function runTasksSequentially() {
await task1();
await task2();
await task3();
console.log('所有任务串行完成');
}
async function task1() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('任务1完成');
resolve();
}, 1000);
});
}
async function task2() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('任务2完成');
resolve();
}, 1000);
});
}
async function task3() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('任务3完成');
resolve();
}, 1000);
});
}
runTasksSequentially();
并行执行
并行执行允许多个异步操作同时进行,当所有操作都完成后再进行下一步。在 Promise 中,可以使用 Promise.all
方法来实现并行执行。
function task1() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('任务1完成');
resolve('任务1结果');
}, 1000);
});
}
function task2() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('任务2完成');
resolve('任务2结果');
}, 2000);
});
}
function task3() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('任务3完成');
resolve('任务3结果');
}, 1500);
});
}
Promise.all([task1(), task2(), task3()])
.then(results => {
console.log('所有任务并行完成,结果:', results);
})
.catch(error => {
console.log('并行任务出错:', error);
});
Promise.all
接受一个 Promise 数组作为参数,返回一个新的 Promise。当所有传入的 Promise 都被解决(resolved)时,新的 Promise 被解决,并以数组形式返回所有 Promise 的结果。如果其中任何一个 Promise 被拒绝(rejected),新的 Promise 也会被拒绝。
使用 async/await
时,同样可以通过 Promise.all
实现并行执行。
async function runTasksInParallel() {
try {
const results = await Promise.all([task1(), task2(), task3()]);
console.log('所有任务并行完成,结果:', results);
} catch (error) {
console.log('并行任务出错:', error);
}
}
async function task1() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('任务1完成');
resolve('任务1结果');
}, 1000);
});
}
async function task2() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('任务2完成');
resolve('任务2结果');
}, 2000);
});
}
async function task3() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('任务3完成');
resolve('任务3结果');
}, 1500);
});
}
runTasksInParallel();
限制并发数
在某些情况下,我们可能需要限制并行执行的异步操作数量,以避免资源耗尽。例如,在进行大量网络请求时,过多的并发请求可能会导致网络拥堵或服务器负载过高。
可以通过队列和 Promise 来实现限制并发数。
function asyncTask(taskId) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(`任务 ${taskId} 完成`);
resolve(taskId);
}, Math.floor(Math.random() * 2000));
});
}
function runTasksWithLimitedConcurrency(tasks, maxConcurrency) {
return new Promise((resolve, reject) => {
let completedCount = 0;
const results = [];
const runningTasks = [];
function startNextTask() {
while (runningTasks.length < maxConcurrency && tasks.length > 0) {
const task = tasks.shift();
const promise = task();
runningTasks.push(promise);
promise
.then(result => {
results.push(result);
completedCount++;
runningTasks.splice(runningTasks.indexOf(promise), 1);
if (completedCount === tasks.length) {
resolve(results);
} else {
startNextTask();
}
})
.catch(error => {
reject(error);
});
}
}
startNextTask();
});
}
const tasks = Array.from({ length: 10 }, (_, i) => () => asyncTask(i + 1));
runTasksWithLimitedConcurrency(tasks, 3)
.then(results => {
console.log('所有任务完成,结果:', results);
})
.catch(error => {
console.log('任务执行出错:', error);
});
在这个例子中,runTasksWithLimitedConcurrency
函数接受一个任务数组和最大并发数作为参数。它通过维护一个正在运行的任务数组 runningTasks
和一个完成任务计数 completedCount
来控制并发。当有任务完成时,从 runningTasks
中移除该任务,并启动下一个任务,直到所有任务都完成。
处理异步错误
Promise 中的错误处理
在 Promise 中,可以通过 catch
方法捕获异步操作中的错误。
function asyncTask() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = false;
if (success) {
resolve('任务成功');
} else {
reject(new Error('任务失败'));
}
}, 1000);
});
}
asyncTask()
.then(result => {
console.log('任务结果:', result);
})
.catch(error => {
console.log('捕获到错误:', error);
});
如果在 then
方法中抛出错误,也会被后续的 catch
方法捕获。
function asyncTask() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('任务成功');
}, 1000);
});
}
asyncTask()
.then(result => {
throw new Error('自定义错误');
console.log('任务结果:', result);
})
.catch(error => {
console.log('捕获到错误:', error);
});
async/await 中的错误处理
在 async/await
中,可以使用 try/catch
块来捕获异步操作中的错误。
async function asyncFunction() {
try {
const result = await new Promise((resolve, reject) => {
setTimeout(() => {
const success = false;
if (success) {
resolve('任务成功');
} else {
reject(new Error('任务失败'));
}
}, 1000);
});
console.log('任务结果:', result);
} catch (error) {
console.log('捕获到错误:', error);
}
}
asyncFunction();
如果不使用 try/catch
块,async
函数中抛出的错误会被返回的 Promise 捕获。
async function asyncFunction() {
const result = await new Promise((resolve, reject) => {
setTimeout(() => {
const success = false;
if (success) {
resolve('任务成功');
} else {
reject(new Error('任务失败'));
}
}, 1000);
});
console.log('任务结果:', result);
throw new Error('自定义错误');
}
asyncFunction()
.catch(error => {
console.log('捕获到错误:', error);
});
异步编程的高级技巧
超时控制
在异步操作中,有时我们需要设置一个超时时间,以防止操作无限期等待。可以通过 Promise.race
和 setTimeout
来实现超时控制。
function asyncTask() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('任务成功');
}, 2000);
});
}
function withTimeout(promise, timeout) {
return Promise.race([
promise,
new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('操作超时'));
}, timeout);
})
]);
}
withTimeout(asyncTask(), 1000)
.then(result => {
console.log('任务结果:', result);
})
.catch(error => {
console.log('捕获到错误:', error);
});
在这个例子中,withTimeout
函数接受一个 Promise 和超时时间作为参数。它使用 Promise.race
同时启动异步任务和一个超时定时器。如果定时器先到期,Promise 会被拒绝并抛出超时错误。
重试机制
对于一些可能会失败的异步操作,我们可以实现重试机制,在操作失败时自动重试一定次数。
function asyncTask() {
return new Promise((resolve, reject) => {
const success = Math.random() < 0.5;
if (success) {
resolve('任务成功');
} else {
reject(new Error('任务失败'));
}
});
}
async function retryAsyncTask(task, maxRetries = 3, delay = 1000) {
let retries = 0;
while (retries < maxRetries) {
try {
return await task();
} catch (error) {
retries++;
console.log(`重试 ${retries} 次,原因:`, error);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new Error('达到最大重试次数,任务失败');
}
retryAsyncTask(asyncTask)
.then(result => {
console.log('任务结果:', result);
})
.catch(error => {
console.log('最终错误:', error);
});
在这个例子中,retryAsyncTask
函数接受一个异步任务、最大重试次数和重试延迟作为参数。它通过 while
循环不断尝试执行任务,直到任务成功或达到最大重试次数。如果任务失败,会等待指定的延迟时间后再次重试。
取消异步操作
在某些情况下,我们可能需要在异步操作完成之前取消它。例如,在用户取消一个正在进行的网络请求时。虽然 JavaScript 本身没有内置的取消机制,但可以通过 AbortController
和 AbortSignal
来实现。
function asyncTask(signal) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
if (signal.aborted) {
reject(new Error('任务已取消'));
} else {
resolve('任务成功');
}
}, 5000);
signal.addEventListener('abort', () => {
clearTimeout(timeoutId);
reject(new Error('任务已取消'));
});
});
}
const controller = new AbortController();
const signal = controller.signal;
asyncTask(signal)
.then(result => {
console.log('任务结果:', result);
})
.catch(error => {
console.log('捕获到错误:', error);
});
// 模拟用户取消操作
setTimeout(() => {
controller.abort();
}, 3000);
在这个例子中,asyncTask
函数接受一个 AbortSignal
对象作为参数。通过监听 signal
的 abort
事件,可以在任务取消时清理资源并拒绝 Promise。AbortController
用于发出取消信号,通过调用 controller.abort()
方法可以取消异步任务。
异步编程与性能优化
减少异步操作的开销
虽然异步编程解决了阻塞问题,但它也带来了一些开销,如函数调用、Promise 创建等。在性能敏感的场景中,应尽量减少不必要的异步操作。
例如,对于一些简单的计算任务,应优先使用同步方式执行,而不是将其包装成异步任务。
// 同步计算
function syncCalculate() {
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += i;
}
return result;
}
// 异步计算(不必要的)
function asyncCalculate() {
return new Promise((resolve) => {
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += i;
}
resolve(result);
});
}
// 直接调用同步函数
const syncResult = syncCalculate();
console.log('同步计算结果:', syncResult);
// 调用异步函数
asyncCalculate()
.then(asyncResult => {
console.log('异步计算结果:', asyncResult);
});
在这个例子中,syncCalculate
函数以同步方式进行简单的累加计算,而 asyncCalculate
函数将同样的计算包装成异步操作。在这种情况下,同步计算的性能更好,因为它避免了 Promise 创建和异步调度的开销。
合理使用并行和串行
在选择并行或串行执行异步操作时,需要考虑任务的性质和资源的限制。
如果任务之间相互独立,且系统资源充足,并行执行可以显著提高效率。例如,同时从多个 API 获取数据。
function fetchData1() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('数据1');
}, 1000);
});
}
function fetchData2() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('数据2');
}, 1500);
});
}
Promise.all([fetchData1(), fetchData2()])
.then(results => {
console.log('所有数据获取完成:', results);
});
然而,如果任务之间存在依赖关系,或者系统资源有限,串行执行可能更合适。例如,一个任务需要使用另一个任务的结果,或者过多的并行请求会导致网络拥堵。
function task1() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('任务1完成');
resolve('任务1结果');
}, 1000);
});
}
function task2(result1) {
return new Promise((resolve) => {
setTimeout(() => {
console.log('任务2完成,使用任务1结果:', result1);
resolve('任务2结果');
}, 1000);
});
}
task1()
.then(result1 => task2(result1))
.then(result2 => {
console.log('所有任务串行完成,最终结果:', result2);
});
优化异步错误处理
异步错误处理也会对性能产生影响。尽量在靠近错误发生的地方处理错误,避免错误在 Promise 链中传递过多层次。
例如,在一个复杂的异步操作链中,如果某个中间步骤可能出错,可以在该步骤直接处理错误,而不是让错误一直传递到最后。
function step1() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve('步骤1结果');
} else {
reject(new Error('步骤1出错'));
}
}, 1000);
});
}
function step2(result1) {
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
// 这里可能依赖步骤1的结果进行操作
const newResult = result1 +'处理后';
resolve(newResult);
} catch (error) {
reject(new Error('步骤2出错,原因:'+ error.message));
}
}, 1000);
});
}
step1()
.then(result1 => step2(result1))
.then(finalResult => {
console.log('所有步骤完成,最终结果:', finalResult);
})
.catch(error => {
console.log('捕获到错误:', error);
});
在这个例子中,step2
函数在内部尝试处理可能的错误,而不是让错误传递到最后统一处理,这样可以减少错误处理的复杂性和性能开销。
通过合理运用这些异步编程的模式与技巧,可以使 JavaScript 应用在处理异步操作时更加高效、稳定和易于维护。无论是处理简单的异步任务还是复杂的异步控制流,选择合适的方法和技术对于提升应用性能和用户体验至关重要。