阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 常见的异步陷阱

常见的异步陷阱

作者:陈川 阅读数:64775人阅读 分类: Node.js

异步编程的常见误区

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

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌