更好轮询,降低你的心智负担
概述
优化轮询代码结构:使用异步迭代器提升可读性和可维护性
我最近遇到需要了一些轮询后端接口的场景,例如监控任务状态或等待异步操作完成。轮询虽然简单,但如果实现不当,会导致代码结构混乱、难以维护,尤其是在处理复杂逻辑时。
本文将探讨一个常见的轮询实现方式及其问题,然后介绍一种优雅的优化方案:利用**异步迭代器(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
循环来消费数据。这种方式将轮询从"命令式"转为"声明式",极大简化了代码结构。
核心思路
- 创建一个函数,返回一个实现了异步迭代器的对象。
- 在迭代器中,使用
while
循环进行轮询,每次yield
当前片段。 - 当字符串拼接完成时,停止迭代。
- 调用方使用
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
中被捕获,无需手动清除资源。 - 内存友好:不需要递归调用,避免了调用栈过深的问题。
- 进度可控:调用方可以在每次迭代中处理进度信息,实现更好的用户体验。
新的文章,最近很累,所以写的东西质量不高,更新也很慢,见谅