常见的异步陷阱
异步编程的常见误区
Node.js 的异步特性是其核心优势,但也带来了不少陷阱。回调地狱是最典型的问题之一。多层嵌套的回调不仅难以阅读,还容易导致错误处理混乱。
fs.readFile('file1.txt', (err, data1) => {
if (err) throw err;
fs.readFile('file2.txt', (err, data2) => {
if (err) throw err;
fs.writeFile('output.txt', data1 + data2, (err) => {
if (err) throw err;
console.log('Done!');
});
});
});
Promise 链中的错误处理
即使使用了 Promise,错误处理不当仍然会导致问题。常见的错误是忘记在 Promise 链末尾添加 catch 处理。
fetch('/api/data')
.then(response => response.json())
.then(data => {
// 处理数据
return processData(data);
})
// 缺少 catch 处理
更糟糕的是,在 then 回调中抛出异常但没有捕获:
fetch('/api/data')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.catch(error => console.error('Fetch failed:', error));
async/await 的隐藏陷阱
async/await 让异步代码看起来像同步代码,但也带来了新的问题。忘记 await 关键字是最常见的错误之一。
async function saveData() {
const data = getData(); // 忘记 await
await db.save(data); // 这里会出错
}
并行执行问题也经常发生:
async function fetchAll() {
const a = await fetch('/api/a'); // 顺序执行,效率低
const b = await fetch('/api/b');
// 应该改为:
// const [a, b] = await Promise.all([
// fetch('/api/a'),
// fetch('/api/b')
// ]);
}
事件循环与微任务
理解事件循环对避免异步陷阱至关重要。process.nextTick 和 Promise 都属于微任务,它们的执行顺序会影响程序行为。
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
// 输出顺序:nextTick, promise
定时器的不准确性也值得注意:
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// 输出顺序可能不同
资源泄漏问题
未正确关闭的资源会导致内存泄漏。数据库连接、文件句柄等都需要显式关闭。
// 错误示例
async function processFile() {
const stream = fs.createReadStream('large.file');
// 忘记关闭流
}
// 正确做法
async function processFile() {
const stream = fs.createReadStream('large.file');
try {
for await (const chunk of stream) {
// 处理数据
}
} finally {
stream.close();
}
}
并发控制挑战
不加控制的并发操作可能导致系统过载。常见的如一次性发起大量数据库查询。
// 错误做法:无限制并发
async function fetchAllUrls(urls) {
return Promise.all(urls.map(url => fetch(url)));
}
// 改进方案:限制并发数
async function fetchWithLimit(urls, limit) {
const results = [];
const executing = [];
for (const url of urls) {
const p = fetch(url).then(r => {
executing.splice(executing.indexOf(p), 1);
return r;
});
executing.push(p);
results.push(p);
if (executing.length >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
上下文丢失问题
在异步回调中,this 指向可能发生变化,导致意外行为。
class Logger {
constructor() {
this.logs = [];
}
log(message) {
this.logs.push(message); // 这里的 this 可能出错
}
// 错误用法
badUsage() {
[1, 2, 3].forEach(function(num) {
this.log(num); // this 不再是 Logger 实例
});
}
// 正确做法
correctUsage() {
[1, 2, 3].forEach(num => {
this.log(num); // 使用箭头函数保持 this
});
}
}
竞态条件
异步操作的执行顺序不确定可能导致竞态条件。比如多个请求同时修改同一数据。
let balance = 100;
async function withdraw(amount) {
if (balance >= amount) {
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 100));
balance -= amount;
return true;
}
return false;
}
// 两个同时提现可能导致余额为负
Promise.all([withdraw(80), withdraw(80)]).then(results => {
console.log(results); // 可能两个都返回 true
console.log(balance); // 可能是 -60
});
未处理的 Promise 拒绝
Node.js 中未处理的 Promise 拒绝会导致警告,甚至可能终止进程。
function riskyOperation() {
return new Promise((resolve, reject) => {
// 可能 reject 但外部没有 catch
if (Math.random() > 0.5) reject(new Error('Failed'));
else resolve('Success');
});
}
// 忘记处理 rejection
riskyOperation().then(console.log);
// 应该总是添加 catch
riskyOperation()
.then(console.log)
.catch(console.error);
定时器累积问题
在循环或频繁调用的函数中使用 setTimeout/setInterval 而不清理,会导致定时器累积。
// 错误示例
function pollUpdates() {
setTimeout(() => {
checkUpdates();
pollUpdates(); // 递归调用但不清理之前的定时器
}, 1000);
}
// 改进方案
let timer;
function betterPoll() {
timer = setTimeout(() => {
checkUpdates();
betterPoll();
}, 1000);
}
// 需要时清除
function stopPolling() {
clearTimeout(timer);
}
异步初始化问题
模块或类中的异步初始化可能导致使用时尚未准备好。
class Database {
constructor() {
this.connection = null;
this.connect(); // 异步初始化
}
async connect() {
this.connection = await createConnection();
}
// 使用时可能 connection 还是 null
async query(sql) {
return this.connection.query(sql);
}
}
// 正确做法:显式初始化
class BetterDatabase {
constructor() {
this.connectionPromise = createConnection();
}
async query(sql) {
const connection = await this.connectionPromise;
return connection.query(sql);
}
}
错误传播中断
在 Promise 链中,抛出错误会中断整个链,但有时我们希望继续执行。
async function processTasks(tasks) {
const results = [];
for (const task of tasks) {
try {
results.push(await performTask(task));
} catch (error) {
console.error(`Task failed: ${task.id}`, error);
// 继续处理下一个任务而不是中断循环
}
}
return results;
}
回调与 Promise 混合
在同一个接口中混用回调和 Promise 会导致混乱和难以调试的问题。
// 反模式:混合风格
function confusingApi(arg, callback) {
if (typeof callback === 'function') {
// 回调风格
process.nextTick(() => callback(null, arg * 2));
} else {
// Promise 风格
return Promise.resolve(arg * 2);
}
}
// 应该统一为 Promise 风格
async function cleanApi(arg) {
return arg * 2;
}
异步堆栈追踪
异步代码的堆栈追踪往往不完整,增加了调试难度。
async function outer() {
await inner();
}
async function inner() {
await deeper();
}
async function deeper() {
throw new Error('Something went wrong');
}
outer().catch(console.error); // 堆栈可能不显示 outer 和 inner
使用 async_hooks 或第三方库可以改善这个问题:
const { AsyncLocalStorage } = require('async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();
async function traced(fn) {
return asyncLocalStorage.run(new Error().stack, async () => {
try {
return await fn();
} catch (error) {
error.stack += '\nAsync context:\n' + asyncLocalStorage.getStore();
throw error;
}
});
}
过度使用 async/await
不是所有情况都需要 async/await,有时简单的 Promise 更合适。
// 不必要的 async
async function getConfig() {
return await readConfigFile();
}
// 等价于
function getConfig() {
return readConfigFile();
}
// 只有在需要 try/catch 时才用 async
async function getConfigSafe() {
try {
return await readConfigFile();
} catch {
return defaultConfig;
}
}
异步迭代器问题
使用异步迭代器时,如果不正确处理可能导致资源未释放。
// 错误示例:可能永远不会结束
async function* generateNumbers() {
let i = 0;
while (true) {
yield i++;
await sleep(100);
}
}
// 使用时应设置中断条件
async function useNumbers() {
for await (const num of generateNumbers()) {
if (num > 10) break; // 必须手动中断
console.log(num);
}
}
异步构造函数限制
JavaScript 中构造函数不能是异步的,这导致一些设计模式受限。
class Resource {
constructor() {
// 不能直接 await
this.initialized = this.init();
}
async init() {
this.data = await loadData();
}
// 使用时必须等待 initialized
async getData() {
await this.initialized;
return this.data;
}
}
// 替代方案:工厂函数
class Resource {
constructor(data) {
this.data = data;
}
static async create() {
const data = await loadData();
return new Resource(data);
}
}
异步测试的挑战
异步测试代码容易编写不完整或错误的断言。
// 错误示例:测试可能通过即使有错误
test('async test', () => {
fetchData().then(data => {
expect(data).toBeDefined();
});
});
// 正确做法:返回 Promise 或使用 async/await
test('async test', async () => {
const data = await fetchData();
expect(data).toBeDefined();
});
// 或者返回 Promise
test('async test', () => {
return fetchData().then(data => {
expect(data).toBeDefined();
});
});
异步代码的性能影响
不当的异步模式可能带来性能问题,如不必要的等待。
// 低效:顺序等待
async function processAll(items) {
const results = [];
for (const item of items) {
results.push(await processItem(item));
}
return results;
}
// 改进:并行处理
async function processAll(items) {
const promises = items.map(item => processItem(item));
return Promise.all(promises);
}
// 带并发控制的版本
async function processWithConcurrency(items, concurrency) {
const results = [];
const executing = new Set();
for (const item of items) {
const p = processItem(item).then(result => {
executing.delete(p);
return result;
});
executing.add(p);
results.push(p);
if (executing.size >= concurrency) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:异步性能优化技巧
下一篇:Buffer的设计初衷