JavaScript: 让异步函数支持超时

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

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

如果你的业务中经常要调用第三方 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 能否正确推导函数类型:

JavaScript: 让异步函数支持超时

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


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

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

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


原文始发于微信公众号(背井):JavaScript: 让异步函数支持超时

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/246908.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!