更好轮询,降低你的心智负担

海龙2025-08-22编程JS/TS

概述

优化轮询代码结构:使用异步迭代器提升可读性和可维护性

我最近遇到需要了一些轮询后端接口的场景,例如监控任务状态或等待异步操作完成。轮询虽然简单,但如果实现不当,会导致代码结构混乱、难以维护,尤其是在处理复杂逻辑时。

本文将探讨一个常见的轮询实现方式及其问题,然后介绍一种优雅的优化方案:利用**异步迭代器(Async Iterator)**来封装轮询逻辑。这种方法不仅简化了调用方的代码,还提高了整体的可读性和可扩展性。

我将避免涉及任何特定框架(如 React 或 Vue)。示例代码会附带详细注释,便于理解。为了使演示更清晰,现在有一个简化的场景:假设我们需要轮询获取字符串片段,直到拼接完整。每次轮询会返回当前可用的字符串片段。

常见的轮询实现及其痛点

在前端开发中,轮询通常是通过 setInterval 或递归调用来实现的。下面是一个典型的示例:轮询获取字符串片段,直到拼接完整。

// 类型定义:字符串片段响应
interface StringResponse {
  successful: boolean;
  data?: {
    fragment: string;
    isComplete: boolean;
    totalLength: number;
  };
}

// 模拟轮询 API 调用
async function getStringFragment(): Promise<StringResponse> {
  // 实际中替换为真实的 API 调用
  return new Promise((resolve) => {
    setTimeout(() => {
      const fragments = ['Hello', ' ', 'World', '!'];
      const randomIndex = Math.floor(Math.random() * fragments.length);

      resolve({
        successful: true,
        data: {
          fragment: fragments[randomIndex],
          isComplete: Math.random() > 0.7, // 30% 概率完成
          totalLength: 12,
        },
      });
    }, 1000);
  });
}

// 轮询函数:使用递归调用实现
async function pollStringUntilComplete(): Promise<string> {
  let result = '';

  const fetchNext = async (): Promise<string> => {
    try {
      const response = await getStringFragment();

      if (!response.successful || !response.data) {
        throw new Error('获取字符串片段失败');
      }

      const { fragment, isComplete } = response.data;
      result += fragment;

      console.log(`已获取片段: "${fragment}",当前结果: "${result}"`);

      if (isComplete) {
        console.log('字符串拼接完成');
        return result;
      } else {
        // 递归调用获取下一个片段
        return fetchNext();
      }
    } catch (error) {
      throw new Error(`获取字符串片段失败: ${error.message}`);
    }
  };

  return fetchNext();
}

// 使用示例
async function main() {
  try {
    const finalString = await pollStringUntilComplete();
    console.log('最终结果:', finalString);
  } catch (error) {
    console.error('轮询出错:', error);
  }
}

main();

这个实现虽然有效,但存在几个问题:

  • 代码嵌套深:递归调用导致逻辑层层嵌套,阅读时需要跟踪调用栈。
  • 状态管理复杂:需要手动维护已拼接的字符串,容易出错。
  • 可扩展性差:如果需要在获取过程中处理中间状态,调用方代码会变得冗长。
  • 可读性低:轮询逻辑与业务逻辑混杂,不易测试或复用。
  • 内存管理:递归调用可能导致调用栈过深,大量片段时可能有问题。

在实际项目中,这种结构会随着需求增加而变得难以维护。我们需要一种更现代、更声明式的方案。

优化方案:使用异步迭代器封装轮询

TypeScript 支持异步迭代器,这是一种通过 Symbol.asyncIterator 实现的可异步迭代对象。它允许我们将轮询逻辑封装成一个可迭代的"流",调用方可以通过 for await...of 循环来消费数据。这种方式将轮询从"命令式"转为"声明式",极大简化了代码结构。

核心思路

  1. 创建一个函数,返回一个实现了异步迭代器的对象。
  2. 在迭代器中,使用 while 循环进行轮询,每次 yield 当前片段。
  3. 当字符串拼接完成时,停止迭代。
  4. 调用方使用 for await...of 来迭代,处理每个片段,并在循环结束后继续业务逻辑。

下面是简化的实现示例,使用相同的字符串片段轮询场景。

// 类型定义:同上
interface StringResponse {
  successful: boolean;
  data?: {
    fragment: string;
    isComplete: boolean;
    totalLength: number;
  };
}

// 模拟轮询 API 调用(同上)
async function getStringFragment(): Promise<StringResponse> {
  return new Promise((resolve) => {
    setTimeout(() => {
      const fragments = ['Hello', ' ', 'World', '!'];
      const randomIndex = Math.floor(Math.random() * fragments.length);

      resolve({
        successful: true,
        data: {
          fragment: fragments[randomIndex],
          isComplete: Math.random() > 0.7,
          totalLength: 12,
        },
      });
    }, 1000);
  });
}

// 优化函数:返回异步迭代器
function createStringPoller(interval: number = 1000): AsyncIterable<string> {
  return {
    async *[Symbol.asyncIterator]() {
      while (true) {
        // 发起轮询请求
        const response = await getStringFragment();

        // 检查响应有效性
        if (!response.successful || !response.data) {
          throw new Error('获取字符串片段失败');
        }

        const { fragment, isComplete } = response.data;

        // yield 当前片段,供调用方处理
        yield fragment;

        // 如果字符串完成,停止迭代
        if (isComplete) {
          return; // 结束迭代器
        }
        // 暂停器,用于控制轮询间隔
        await new Promise((resolve) => setTimeout(resolve, interval));
      }
    },
  };
}

// 使用示例:使用 for await...of 消费迭代器
async function main() {
  try {
    const stringPoller = createStringPoller();
    let result = '';

    for await (const fragment of stringPoller) {
      result += fragment;
      console.log(`获取到片段: "${fragment}",当前结果: "${result}"`);
    }

    console.log('字符串拼接完成:', result);
  } catch (error) {
    console.error('轮询出错:', error);
  }
}

main();

为什么这个方案更好?

  • 简化调用方代码for await...of 使轮询看起来像一个简单的循环,隐藏了轮询逻辑的细节。调用方只需处理每个片段,并在循环结束后继续执行。
  • 提高可读性:轮询逻辑被封装在迭代器中,业务逻辑(如拼接字符串)集中在循环体内,易于理解和修改。
  • 类型安全:TypeScript 确保了迭代器的类型,减少运行时错误。
  • 可扩展性强:可以轻松添加错误重试、最大重试次数、延迟控制等功能,而不影响调用方。
  • 错误处理优雅:抛出错误会直接在 for await 中被捕获,无需手动清除资源。
  • 内存友好:不需要递归调用,避免了调用栈过深的问题。
  • 进度可控:调用方可以在每次迭代中处理进度信息,实现更好的用户体验。

新的文章,最近很累,所以写的东西质量不高,更新也很慢,见谅

最后更新日期 9/11/2025, 4:13:51 AM