LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

【Web前端开发】JavaScript 不要再这样编写 async/await

admin
2024年12月30日 12:0 本文热度 386

最开始接触 async/await 时,很多人都会发出“终于有这个功能了!”的感叹。它的语法清晰、可读性强,用起来直观又顺手。

然而,用得越久,就会发现一些常见的“坑”时常在各种项目里出现:有些是代码审查时发现的,有些是和同事讨论时暴露的问题。这些都说明异步编程本质上并不简单。

下文就结合实际经验,列出了一些常见的异步陷阱,以及更高级的用法与思考方式,让代码更健壮,也更易维护。


从回调地狱到 async/await

还记得当初的回调地狱吗?JavaScript 进化到现在,已经让我们避免了深层嵌套的回调结构。

但功能变强大了,责任也跟着变大。下面是 async/await 中常见的三大“致命罪状”。


1. 同步式的瀑布请求

糟糕示例:顺序等待

在代码审查里经常看到这样的场景:本来可以并发执行的请求,却被一个接一个地串行处理。

// 🚫 不推荐
async function loadDashboard({
  const user = await fetchUser();
  const posts = await fetchPosts();
  const notifications = await fetchNotifications();
}

如果每个请求都要 200ms,这里就总共要 600ms,给用户的体验自然不佳。

改进:并发执行

对于相互独立的操作,应该使用并发来节省时间:

// ✅ 建议用 Promise.all
async function loadDashboard({
  const [user, posts, notifications] = await Promise.all([
    fetchUser(),
    fetchPosts(),
    fetchNotifications()
  ]);
}

同样是获取用户、帖子和通知,响应速度立刻加快三倍左右。不过,并发并非万能。

  • 依赖关系:如果一个请求需要另一个请求返回的数据,就必须顺序执行。
  • 竞态条件:如果多个请求会同时修改某个共享资源,可能导致数据不一致。
  • 资源限制:并发过多会给服务器或浏览器带来压力。
举例:危险的并发
// 🚫 并行可能导致竞态问题
await Promise.all([
  updateUserProfile(userId, { name'New Name' }),
  updateUserProfile(userId, { email'new@email.com' })
]);

// ✅ 先后执行以防数据冲突
const user = await updateUserProfile(userId, { name'New Name' });
await updateUserProfile(userId, { email'new@email.com' });

如果并行更新同一个用户资料,服务器可能会出现覆盖数据的情况。必须根据业务逻辑判断能否并发执行。


2. 隐形错误:如何平衡异常处理

常见误区:把错误直接“吞掉”

不少人喜欢在 catch 块里写个简单的 console.error(error),然后返回 null,让外层调用时貌似一切正常。

// 🚫 隐形错误
async function fetchData({
  try {
    const response = await fetch('/api/data');
    return await response.json();
  } catch (error) {
    console.error(error);
    return null;  // 🚨 难以排查的隐患
  }
}

看似“处理”了错误,但实际上把错误原因都藏起来了。网络断了?JSON 解析失败?服务器返回 500?外部代码只能拿到 null,毫无头绪。

更好的做法:区分场景处理

返回空值并非一直不对。如果它只是一个不关键的功能,比如推荐列表或活动通知,给用户一个空状态也许是更友好的方式。但如果是核心数据,就应该抛出异常或者做更明确的错误处理,让上层逻辑感知到问题。

高级开发者通常会这样写:

// ✅ 有针对性的错误处理
async function fetchData(options = {}{
  const { isCritical = true } = options;
  
  try {
    const response = await fetch('/api/data');
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    console.error('Data fetch failed:', error);
    
    if (isCritical) {
      // 对关键数据抛出自定义错误,交由更高层级处理
      throw new ApiError('Failed to fetch critical data', { cause: error });
    } else {
      // 非关键数据,可返回一个降级的默认值
      return { type'fallback'data: [] };
    }
  }
}

// 用法示例:
// 关键数据(出错时会抛异常)
const userData = await fetchData({ isCriticaltrue });

// 非关键通知数据(出错时返回空列表)
const notifications = await fetchData({ isCriticalfalse });

这样就能同时兼顾稳定性和可维护性。关键数据绝不能“悄悄失败”,而次要功能可以“优雅退化”。


3. 内存泄漏的陷阱和现代化的清理方式

典型误区:无休止的轮询

假设写了一个定时轮询,几秒钟拉取一次数据:

// 🚫 隐形的内存杀手
async function startPolling({
  setInterval(async () => {
    const data = await fetchData();
    updateUI(data);
  }, 5000);
}

表面看上去没什么问题,但这样会导致:

  • 组件或页面卸载后依然在轮询
  • 如果 fetchData() 执行得很慢,可能会同时发起多次请求
  • 更新 UI 时,目标 DOM 甚至可能已经被移除
改进:AbortController + 轮询管理

下面这个示例借助 AbortController 实现了更安全的轮询:

class PollingManager {
  constructor(options = {}) {
    this.controller = new AbortController();
    this.interval = options.interval || 5000;
  }

  async start() {
    while (!this.controller.signal.aborted) {
      try {
        const response = await fetch('/api/data', {
          signalthis.controller.signal
        });
        const data = await response.json();
        updateUI(data);
        
        // 等待下一次轮询
        await new Promise(resolve => setTimeout(resolve, this.interval));
      } catch (error) {
        if (error.name === 'AbortError') {
          console.log('Polling stopped');
          return;
        }
        console.error('Polling error:', error);
      }
    }
  }

  stop() {
    this.controller.abort();
  }
}

// 在 React 组件中使用
function DataComponent({
  useEffect(() => {
    const poller = new PollingManager({ interval5000 });
    poller.start();
    
    // 组件卸载时停止轮询
    return () => poller.stop();
  }, []);
  
  return <div>Data: {/* ... */}</div>;
}

通过使用 AbortController,可以在需要时终止请求并及时释放资源,更好地控制组件的生命周期和内存占用。


高级开发者的工具箱

1. 重试(Retry)模式

网络环境不稳定或第三方服务时好时坏的情况下,只尝试一次就放弃不是好办法。可以加上重试和退避策略:

async function fetchWithRetry(url, options = {}{
  const { maxRetries = 3, backoff = 1000 } = options;
  
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fetch(url);
    } catch (error) {
      if (attempt === maxRetries - 1throw error;
      // 退避延迟,指数增长
      await new Promise(r => setTimeout(r, backoff * Math.pow(2, attempt)));
    }
  }
}

除了基本的指数退避,还可以考虑:

  • 避免过度重试导致资源浪费
  • 区分哪些错误类型才需要重试
  • 使用断路器(Circuit Breaker)模式保护系统
  • 加入随机抖动(Jitter)防止大量请求同时重试

2. 资源管理

启动异步操作简单,关键是如何“优雅地”停止它们。通过统一的 ResourceManager 或类似模式,可以集中处理一些关闭连接、清理定时器、取消任务等逻辑:

class ResourceManager {
  cleanup() {
    // 在这里集中处理各种操作的终止和释放
  }
}

真实场景中的模式

1. 统一的加载状态管理

不要在每个组件都写一堆 “正在加载”、“错误” 判断。可以抽象出一个自定义 Hook 或者统一的加载管理逻辑:

const { data, loading, error } = useAsyncData(() => fetchUserData());

这样可以:

  • 保持加载和错误状态处理的一致性
  • 便于集中管理和优化
  • 在组件卸载时自动清理

2. 数据同步器(Data Synchronizer)

对于实时性要求高的应用,与其一个个写请求,不如建立一个数据同步管理器,统一处理轮询/订阅/数据合并等逻辑:

const syncManager = new DataSyncManager({
  onSync(data) => updateUI(data),
  onError(error) => showError(error),
  syncInterval5000
});

几条核心原则

  1. 并发原则

    • 让互不依赖的操作同时执行
    • 小心竞态和依赖顺序
    • 不要为了并发而并发,要考虑业务逻辑
  2. 弹性原则(Resilience)

    • 及时重试和错误处理
    • 做好网络或服务不可靠的准备
    • 避免一个错误拖垮整个系统
  3. 资源管理原则

    • 主动清理异步操作
    • 终止不再需要的请求
    • 防止内存泄漏
  4. 用户体验原则

    • 有意义的加载提示和错误信息
    • 保证核心功能的可用性
    • 在可能的情况下提供取消或中断操作

常见问题

1. 什么时候用 Promise.all,什么时候用 Promise.allSettled

  • Promise.all 适合所有请求都必须成功的场景
  • Promise.allSettled 允许部分失败,适合容忍部分请求出错的需求
  • Promise.race 则常用于超时等情况

2. 在 React 中如何优雅地清理?

  • 善用 useEffect 的返回值进行清理
  • 使用 AbortController 终止 HTTP 请求
  • 组件卸载时,保证所有定时器、订阅或轮询都能停止

展望

  • 检查代码:查找本可并发却写成串行的请求;检查错误处理是否含糊不清;关注异步操作的清理是否充分。
  • 改进模式:引入更健壮的错误处理,增加重试逻辑,优化资源释放。
  • 考虑扩展性:当用户量或请求量激增时,如何保证依旧能流畅运行?如果某些服务变慢甚至挂掉,该如何部分降级?

为什么要在意这些细节

或许有人会说,“我的小项目没这么复杂,用不着搞这些”。但真正的好代码是能经得住放大和演进的。

  • 可扩展性:面对更多用户和请求,系统能否稳健地运行
  • 用户体验:高并发、良好错误处理能让应用体验更流畅
  • 开发者体验:清晰的异步逻辑有助于日后维护和团队协作
  • 资源利用:合理的并发和清理机制能节约服务器和客户端资源

这些并不是纸上谈兵,而是大量实战总结出来的硬道理。随着项目的规模和复杂度不断提升,这些异步编程模式会是你写出高质量前端代码的核心基石。


该文章在 2024/12/30 12:24:25 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2025 ClickSun All Rights Reserved