如何让通用算法函数同时兼容同步与异步函数?
我明白你的痛点——写两套几乎一模一样的同步/异步函数实在太冗余了,咱们来看看几个能解决这个问题的方案,你可以根据自己的需求选最合适的。
方案一:统一异步处理(最简单,但需要全量await)
如果能接受不管传入的是同步还是异步函数,都返回Promise(也就是调用时都要加await),那这个方案最省心。核心思路是用Promise.resolve()把同步函数的返回值也包装成Promise,然后统一用async/await处理:
async function findZero(f: (x: number) => number | Promise<number>): Promise<number> { for (let i = 0; i < 1000; i++) { // 不管f是同步还是异步,都用Promise.resolve统一包装,再await const fi = await Promise.resolve(f(i)); if (fi === 0) return i; } return -1; } // 调用示例 console.log(await findZero(x => 5 - x)); // 输出5 console.log(await findZero(x => new Promise(resolve => resolve(5 - x)))); // 输出5
这个方案的优点是代码极简,完全没有重复逻辑;缺点是哪怕传入同步函数,你也必须用await才能拿到结果,可能不符合你“同步调用直接返回值”的预期。
方案二:函数重载+类型判断(贴合需求,类型安全)
如果想要同步函数调用直接返回普通值、异步函数返回Promise,那可以用TypeScript的函数重载来定义不同的签名,再通过类型判断内部走不同的逻辑。为了减少代码重复,还可以把核心循环逻辑抽离出来:
第一步:抽离通用循环逻辑
先把同步和异步的循环逻辑单独封装,这样主函数只需要做类型判断和调用:
// 同步循环:处理同步函数 function syncLoop<T>(start: number, end: number, getValue: (i: number) => T, shouldStop: (val: T) => boolean): number { for (let i = start; i < end; i++) { const val = getValue(i); if (shouldStop(val)) return i; } return -1; } // 异步循环:处理异步函数 async function asyncLoop<T>(start: number, end: number, getValue: (i: number) => Promise<T>, shouldStop: (val: T) => boolean): Promise<number> { for (let i = start; i < end; i++) { const val = await getValue(i); if (shouldStop(val)) return i; } return -1; }
第二步:实现兼容同步/异步的主函数
用函数重载明确两种调用签名,内部判断传入函数的返回值类型,调用对应的循环逻辑:
// 同步版本的函数签名 function findZero(f: (x: number) => number): number; // 异步版本的函数签名 function findZero(f: (x: number) => Promise<number>): Promise<number>; // 通用实现 function findZero(f: (x: number) => number | Promise<number>): number | Promise<number> { // 调用一次函数,判断返回值是同步值还是Promise const testResult = f(0); if (typeof testResult === 'number') { // 同步函数,调用同步循环 return syncLoop(0, 1000, f as (x: number) => number, val => val === 0); } else { // 异步函数,调用异步循环 return asyncLoop(0, 1000, f as (x: number) => Promise<number>, val => val === 0); } } // 调用示例 console.log(findZero(x => 5 - x)); // 直接输出5,无需await console.log(await findZero(x => new Promise(resolve => resolve(5 - x)))); // 输出5
这个方案完美贴合你的需求:同步调用直接返回结果,异步调用返回Promise,而且核心逻辑只需要维护一套(抽离的循环函数),后续修改算法逻辑也只需要改这两个循环函数即可。
方案三:主函数+转换工具(适合复杂算法)
如果你的算法逻辑非常复杂,不想在一个函数里处理类型判断,也可以写一个核心的异步主函数,再用工具函数转换成同步/异步变体(不过要注意:JavaScript里没法真正同步等待Promise,所以同步转换只适合原本就是同步的函数):
// 核心异步主函数 async function masterFindZero(f: (x: number) => Promise<number>): Promise<number> { for (let i = 0; i < 1000; i++) { const fi = await f(i); if (fi === 0) return i; } return -1; } // 转换为同步版本:只接受同步函数,内部包装成Promise后调用主函数,再同步获取结果(注意:仅适用于Node.js环境,浏览器不支持同步Promise等待) function convertToSync<T, R>(masterFn: (f: (x: T) => Promise<R>) => Promise<R>) { return (f: (x: T) => R): R => { // Node.js下可以用util模块的同步等待方式 const promise = masterFn(x => Promise.resolve(f(x))); return require('util').promisify((cb) => promise.then(res => cb(null, res), cb))(); }; } // 转换为异步版本:直接复用主函数 const asyncFindZero = masterFindZero; // 同步版本 const syncFindZero = convertToSync(masterFindZero); // 调用示例 console.log(syncFindZero(x => 5 - x)); // 输出5 console.log(await asyncFindZero(x => new Promise(resolve => resolve(5 - x)))); // 输出5
这个方案的局限性在于同步转换依赖Node.js环境,浏览器里没法实现真正的同步等待Promise,所以如果是前端项目,还是方案二更合适。
备注:内容来源于stack exchange,提问作者Niobos

