代碼審查,異步調用的常見問題剖析

先來看一段代碼,就是一小段而已:

 export function loginWithWx() {
     wx.showLoading({ title: "登錄中..." });
     wx.login({
         success: res => {
             wx.request({
                 url: `${apiRoot}wx/${res.code}`,
                 method: "get",
                 success: res => {
                     const { data } = res;
                     const jwt = app.globalData.jwt = data?.jwt;
                     if (jwt) {
                         wx.reLaunch({ url: "../index/index" });
                         wx.hideLoading();
                    }
                     else {
                         showMessage(data.message || "登錄時發生錯誤");
                         wx.hideLoading();
                    }
                },
                 fail: res => {
                     showMessage("請求超時,請稍後重試");
                }
            });
             wx.hideLoading();
        },
         fail: res => {
             console.log(res);
        }
    });
     wx.hideLoading();
 }

這段代碼乍一看,似乎沒毛病。但是稍微思考一下,就能發現問題了。

首先,最直觀的問題:縮進太深。縮進最深的地方是 24 個空格,也就是 6 層。一般我們認爲 3 層以內的縮進比較容易閱讀,超過 3 層應該考慮使用“Extract Method”方法進行重構。

接下來,看外層邏輯:

 wx.showLoading()
 wx.login()
 wx.hideLoading()

這是期望的執行順序。

注意到 wx.login 是一個異步過程,所以實際上 hideLoading() 並不會等登錄過程結束就關閉了加載提示。所以第 2 個問題是忽略了異步執行的順序

馬上可以想到使用 wx.login()complete 參數來解決:

 wx.showLoading();
 wx.login({
     complete: () => wx.hideLoading()
 });

不過馬上就引出了下一個問題:complete 還是太快

爲什麼?我們再把內部的邏輯結構清理出來:

 wx.login({
     success: () => {
         wx.request({
             success: () => { },
             fail: () => { }
        })
    },
     fail: () => { }
 })

注意到 wx.request 仍然是一個異步過程,所以 wx.loginsuccess 會立即結束,觸發 complete。而這時候 wx.request 可能還在等待服務器響應。

那麼是不是應該把 wx.hideLoading() 放到內部邏輯中去?理論上來說,是的!

但實際情況是,內部邏輯分支較多,深次較深,既有同步邏輯,也有異步邏輯……考慮應該放在哪些地方,需要非常的謹慎。實際上,案例中的代碼就已經在內部邏輯中放了一些 wx.hideLoading(),只不過

  1. 覆蓋不全;

  2. 因爲最外層的 hideLoading()  提前執行,失效了。

  3. 違反了規範性約束:成對邏輯應該儘量避免一對多的情況

解釋一下第 3 點,就是說:一個 showLoading() 最好只對應一個 hideLoading()。考慮到邏輯的複雜性,這不是強制約束規則,但應該儘量去避免。

處理的辦法是,重構,將內部邏輯拆分出來;然後,將完成事件處理邏輯作爲一個參數,一層層的往裏傳:

 function appLogin(params, complete) {
 //                       ^^^^^^^^
     wx.request({
         ...params,
         complete: complete
 //               ^^^^^^^^
    });
 }
 
 function wxLogin(params, complete) {
 //                       ^^^^^^^^
     wx.login({
         ...params,
         success: () => appLogin({}, complete),
 //                                 ^^^^^^^^
         fail: () => complete()
 //                 ^^^^^^^^^^
 //     complete: complete // ✗
 //     注意:由於 success 和 fail 裏存在異步處理,不能直接使用 complete 事件。
 //           原因在前面已經說了。
    });
 }
 
 wx.showLoading();
 wxLogin({}, () => wx.hideLoading());
 //         ^^^^^^^^^^^^^^^^^^^^^^ 傳入的 complete

顯然在當前的技術環境中,這並不是最優方案,還可以繼續優化——反正都要封裝,乾脆封裝成 Promise。然後通過 await 調用轉換成同步語法,處理起來會輕鬆得多。封裝的具體過程在前兩篇文章中有詳細的講解,這裏就不贅述了。總之,我們封裝了 wx 的異步版本 awx,在這裏用就好:

 export async function asyncLoginWithWx() {
     wx.showLoading({ title: "登錄中..." });
 
     try {
         return await internalProcess();
    } catch (err) {
         showMessage("請求超時,請稍後重試");
    } finally {
         wx.showLoading();
    }
 
     // 把內部邏輯用個局部函數封裝起來,
     // 主要是爲了讓 try ... catch ... 看起來清晰一些
     async function internalProcess() {
         const { code } = await awx.login();
 
         const { data } = awx.request({
             url: `${apiRoot}wx/${code}`,
             method: "get",
        });
 
         const jwt = app.globalData.jwt = data?.jwt;
         if (jwt) {
             wx.reLaunch({ url: "../index/index" });
        } else {
             showMessage(data.message || "登錄時發生錯誤");
        }
    }
 }




喜歡此文,點個在看 ⇘

支持作者,賞個咖啡豆 ⇓


本文分享自微信公衆號 - 邊城客棧(fancyidea-full)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章