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

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

其实不用太在乎“小程序”这件事情,由于“小程序”在文章中只是一个开发场景,咱们实际解决的问题并不是只在小程序中才会遇到,而解决问题的手段彻底与小程序无关!ios

1. 问题

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

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

若是须要在请求的过程当中取消请求,就会用到 wx.request() 的返回值:github

const requestTask = wx.request(...);
if (...) {
    // 由于某些缘由须要取消此次请求
    requestTask.abort();
}

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

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 种方法不可选。segmentfault

第 2 种方法可行,不须要原返回值的时候,直接使用便可。可是须要原返回值的时候,稍嫌麻烦,须要先产生一个容器对象传入。微信小程序

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

如今咱们来实现第 3 种方法,改造 wxPromisify()promise

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 对象带出来和经过一个专门的“容器”对象带出来,本质是同样的,因此就很少说了。


边城客栈

请关注公众号边城客栈

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

相关文章
相关标签/搜索