改進異步封裝:處理帶返回值的異步調用

最近幾篇文章都跟微信小程序開發有關,因此有人就問:「小程序不懂啊,能不能寫點別的?」。

其實不用太在乎「小程序」這件事情,由於「小程序」在文章中只是一個開發場景,咱們實際解決的問題並不是只在小程序中才會遇到,而解決問題的手段徹底與小程序無關!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 對象帶出來和經過一個專門的「容器」對象帶出來,本質是同樣的,因此就很少說了。


邊城客棧

請關注公衆號邊城客棧

看完了先別走,點個贊 ⇓ 啊,讚揚 ⇘ 就更好啦!

相關文章
相關標籤/搜索