改进异步封装:处理带返回值的异步调用

最近几篇文章都跟微信小程序开发有关,所以有人就问:“小程序不懂啊,能不能写点别的?”。

其实不用太在意“小程序”这件事情,因为“小程序”在文章中只是一个开发场景,我们实际解决的问题并非只在小程序中才会遇到,而解决问题的手段完全与小程序无关!

1. 问题

Proxy 封装微信小程序的异步调用 中留下了一个问题:

wx.request() 这种原本就有返回值的情况,该如何封装呢?

如果需要在请求的过程中取消请求,就会用到 wx.request() 的返回值:

const requestTask = wx.request(...);
if (...) {
    // 因为某些原因需要取消这次请求
    requestTask.abort();
}

封装过后的 awx.request() 会返回一个 Promise 对象,跟 wx.request() 原来的返回值毫无关系。如果想要能够取消请求,就必须将 wx.request() 原来的返回值带出来,应该怎么办?

function wxPromisify(fn) {
    return async function (args) {
        return new Promise((resolve, reject) => {
            const originalResult = fn({
//          ^^^^^^^^^^^^^^^^^^^^^^^
//          怎么把 originalResult 带出去?
                ...(args || {}),
                success: res => resolve(res),
                fail: err => reject(err)
            });
        });
    };
}

2. 可选方案

也不卖关子了,这里有几个方案可选:

  1. 返回对象或数组,解构后使用。比如返回 { promise, originalResult}[promise, originalResult]
  2. 通过一个“容器”参数将返回值带出来,比如 awx.request(params, outBox = {}),在处理时为 outBox 赋值:outBox.originalResult
  3. JS 是动态类型,可以直接修改 Promise 对象,为其附加属性:promise.originalResult = ...

从使用者的角度来考虑,多数时候是不需要原返回值的,这时候是肯定是希望 await awx.request(),而不是先解构再 await(或 then()),所以,第 1 种方法不可选。

第 2 种方法可行,不需要原返回值的时候,直接使用即可。但是需要原返回值的时候,稍嫌麻烦,需要先产生一个容器对象传入。

第 3 种方法使用起来应该是最“无感”的。无论如何,原值随 Promise 对象带出来了,用或是不用,请便!

现在我们来实现第 3 种方法,改造 wxPromisify()

3. 失败的尝试

一开始想得很简单,原来直接 return new Promise(),现在加个临时变量应该就可以吧:

function wxPromisify(fn) {
    return async function (args) {
        const promise = new Promise((resolve, reject) => {
//      ^^^^^^^^^^^^^^^^
            promise.originalResult = fn({
//          ^^^^^^^^^^^^^^^^^^^^^^^^^
                ...(args || {}),
                success: res => resolve(res),
                fail: err => reject(err)
            });
        });
        
        return promise;
//      ^^^^^^^^^^^^^^^
    };
}

然后得到一个错误:

TypeError: Cannot set property 'originalResult' of undefined

这个错很好理解,也很容易改……不过确实也很容易犯!

本来是认为 promise 是个局部变量,可以直接访问,所以在其子作用域中使用是没问题。但是这里忽略了这个子作用域是在构造函数中。来大概分析一下:

new Promise() 需要一个函数(假设叫 factory)作为参数,但是这个 factory 执行的时机是什么?注意到 new Promise() 产生 Promise 实例之后,我们再没有主动调用这个实例的任何方法,所以可以断定,factory 是在构造的过程中执行的。换句话说,这时候 Promise 实例还没产生呢,promise 引用的是 undefined

4. 成功的尝试

既然已经知道问题所在,我们接着分析。

构造 Promise 实例的过程中调用了 factory,而 factory 的在函数体中直接执行了 fn,可以立即拿到 fn 的返回值,所以这个 Promise 实例构造完成之后,是可以拿到原返回值的。

现在来修改一下代码:

function wxPromisify(fn) {
    return async function (args) {
        let originalResult;
//      ^^^^^^^^^^^^^^^^^^^
        const promise = new Promise((resolve, reject) => {
            originalResult = fn({
//          ^^^^^^^^^^^^^^
                ...(args || {}),
                success: res => resolve(res),
                fail: err => reject(err)
            });
        });

        promise.originalResult = originalResult;
//      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        return promise;
    };
}

我们需要在 new Promise() 之后对 promise.originalResult 赋值,而这个“值”产生于 new Promise() 的过程中,那么再加个局部变量 originalResult 把它带出来就好。

搞定!

5. 搞笑却又应该严肃对待的事情

本来应该结束了,但我猜一定会有人这么干(因为我在其他场景下见过):

注意:下面这个是错误示例!
function wxPromisify(fn) {
    return async function (args) {
        let promise = new Promise();
//      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        promise = new Promise((resolve, reject) => {
//      ^^^^^^^^^^
            promise.originalResult = fn({ ... });
//          ^^^^^^^^^^^^^^^^^^^^^^
        });

        return promise;
    };
}

这样做不会产生前面提到的 TypeError,但是外面拿到的 Promise 对象却并不携带 originalResult。具体原因跟上面失败的那次尝试一样,所以不再详述,只提醒一下:这里产生了两个 Promise 对象

6. 再啰嗦一下

这次带出原返回值是以 wx.request() 为例,其返回值的主要用途是提供 .abort() 方法用于取消请求。这个应用场景其实和 Axios 处理“取消请求 (Cancellation)”类似,所以不妨参考 Axios 通过 cancelToken 实现的方法。cancelToken 的实质就是前面提到的第 2 种方法 —— 传入“容器”对象把需要的东西带出来。通过 Promise 对象带出来和通过一个专门的“容器”对象带出来,本质是一样的,所以就不多说了。


边城客栈

请关注公众号边城客栈

看完了先别走,点个赞 ⇓ 啊,赞赏 ⇘ 就更好啦!

阅读 934

推荐阅读
边城客栈
用户专栏

全栈技术专栏

3130 人关注
77 篇文章
专栏主页