暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

JavaScript: 让异步函数支持超时

背井 2021-05-25
3645

这是前几天遇到的一道面试题。我们稍微展开一点来说。

超时 这个话题说大不大说小不小。

如果你的业务中经常要调用第三方 API,考虑到系统的稳定性,添加超时限制总归是好的。

试想,你调用一个 API,本以为它会极快的返回结果,最终却是等了十几分钟,那整个服务基本上都得在那里等着。而有了 超时 机制,让 API 的调用在超时时提前结束,后续的代码做一些出错报告/弥补。接着代码去做别的事,与在那傻傻等着相比,这样能显著提高服务的吞吐量。

另外,你在对外提供 API 时,允许消费者设定 超时 参数,也说明你用心了不是?

那么,我们看看怎么让一个异步函数支持超时。

让异步函数支持超时

你可能有很多的函数,而其超时的特点基本是一样的:异步函数执行超过指定的时间没有结束时,报超时异常。

所以为了达到代码复用的最大化,我们可以写一个高阶函数,它接收一个异步函数和超时时间,返回一个会超时的异步数。(相当于一个异步函数装饰器)

一个初步的实现如下:

function timeout(fun, time{    return (...args) => {        return Promise.race([            fun(...args),            new Promise((_, rej) => {                setTimeout(rej, time, new Error('超时了!'));            }),        ]);    };}

这个实现有 2 点要关注的:

  1. Promise.race()
    ,它接收一个 iterable
    对象,返回最先被 settle
    的那个 promise
    。所以,如果异步函数晚于 timeout
    ,函数的执行就会报 超时了! 异常。
  2. ...args
    变参。这是因为我们并不知道异步函数接收几个参数,用变参刚好达到目的。

除了上面的参考实现,下面再提供几个思考点。

拓展思考1:不使用 Promise.race()
,如何实现上面的 timeout()
函数?

读者可以先想一会,再继续往下读。

我想到的一个方案是:

function timeout(fun, time{    return (...args) => {        return new Promise((res, rej) => {            fun(...args).then(res, rej);            setTimeout(rej, time, new Error('超时了'));        });    };}

这相当于重新实现了 Promise.race()
方法,给我的感觉它比上面的实现可读性更好。你说呢?

拓展思考2:怎么用 TypeScript 重写上面的函数?

因为 timeout
是一个高阶函数,在用 TypeScript 声明类型时,我们要考虑到异步函数和包装后的异步函数在入参和出参的类型上的一致性。

要是你会怎么做?

请先思考一会。

我这里以方案2为例,提供一个 TypeScript 版本的参考实现:

function timeout<F extends (...args: any[]) => Promise<any>>(fun: F, time: number{    return (...args: Parameters<F>) => {        return new Promise((res, rej) => {            fun(...args).then(res, rej);            setTimeout(rej, time, new Error('超时了'));        }) as ReturnType<F>;    };}

几点要注意的是:

  1. Parameters<F>
    用来获取异步函数的入参类型。
  2. ReturnType<F>
    用来获取异步函数的返回值类型。
  3. 为了让高阶函数返回的函数的返回值和异步函数一致,我们使用 as ReturnType<F>
    来指明其类型。

如果你不喜欢 as ReturnType<F>
这种写法,还可以主动提取异步函数返回值的类型:

function timeout<F extends (...args: any[]) => Promise<any>>(fun: F, time: number{    return (...args: Parameters<F>) => {        // 提取 Promise 里包裹的类型        type Unwrap<T> = T extends Promise<infer R> ? R : unknown;        return new Promise<Unwrap<ReturnType<F>>>((res, rej) => {            fun(...args).then(res, rej);            setTimeout(rej, time, new Error('超时了'));        });
    };
}

不过我发现前者更易读一些,后面这种理解就好,不建议使用。

再看看,在使用时 TypeScript 能否正确推导函数类型:

f
的类型是正确的 ,没问题 !


希望这个分享对你有帮助!

其实还有一种通过继承 Promise
的方式,实现一个 TimeoutPromise
,来解决异步超时的问题。

详见我的另一篇文章:关于Promise和Async/Await你可能不知道的事


文章转载自背井,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论