使用Await減少回撥巢狀

人人網FED發表於2017-10-31

在開發的時候,有時候需要發很多請求,然後經常會面臨巢狀回撥的問題,即在一個回撥裡面又嵌了一個回撥,導致程式碼層層縮排得很厲害,如下程式碼所示:

ajax({
    url: "/list",
    type: "GET",
    success: function(data) {
       appendToDOM(data);
        ajax({
            url: "/update",
            type: "POST",
            success: function(data) {
                util.toast("Success!");
            })
        });
    }
});複製程式碼

這樣的程式碼看起來有點吃力,這種非同步回撥通常可以用Promise優化一下,可以把上面程式碼改成:

new Promise(resolve => {
    ajax({
        url: "/list",
        type: "GET",
        success: data => resolve(data);
    })
}).then(data => {
   appendToDOM(data);
    ajax({
        url: "/update",
        type: "POST",
        success: function(data) {
            util.toast("Successfully!");
        })  
    }); 
});複製程式碼

Promise提供了一個resolve,方便通知什麼時候非同步結束了,不過本質還是一樣的,還是使用回撥,只是這個回撥放在了then裡面。


當需要獲取多次非同步資料的時候,可以使用Promise.all解決:

let orderPromise = new Promise(resolve => {
    ajax("/order", "GET", data => resolve(data));
});
let userPromise = new Promise(resolve => {
    ajax("/user", "GET", data => resolve(data));
});

Promise.all([orderPromise, userPromise]).then(values => {
    let order = values[0],
         user = values[1];
});複製程式碼

但是這裡也是使用了回撥,有沒有比較優雅的解決方式呢?


ES7的await/async可以讓非同步回撥的寫法跟寫同步程式碼一樣。第一個巢狀回撥的例子可以用await改成下面的程式碼:

// 使用await獲取非同步資料
let leadList = await new Promise(resolve => {
    ajax({
        url: "/list",
        type: "GET",
        success: data => resolve(data);
    });
});

// await讓程式碼很自然地像瀑布流一樣寫下來 
appendToDom(leadList);
ajax({
    url: "/update",
    type: "POST",
    success: () => util.toast("Successfully");
});
複製程式碼

Await讓程式碼可以像瀑布流一樣很自然地寫下來。

第二個例子:獲取多次非同步資料,可以改成這樣:

let order = await new Promise(
           resolve => ajax("/order", data => resovle(data))),

    user = await new Promise(
           resolve => ajax("/user", data => resolve(data)));

// do sth. with order/user複製程式碼

這種寫法就好像從本地獲取資料一樣,就不用套回撥函式了。

Await除了用在發請求之外,還適用於其它非同步場景,例如我在建立訂單前先彈一個小框詢問使用者是要建立哪種型別的訂單,然後再彈具體的設定訂單的框,所以按正常思路這裡需要傳遞一個按鈕回撥的點選函式,如下圖所示:

但其實可以使用await解決,如下程式碼所示:

let quoteHandler = require("./quote");
// 彈出框詢問使用者並得到使用者的選擇
let createType = await quoteHandler.confirmCreate();複製程式碼

quote裡面返回一個Promise,監聽點選事件,並傳遞createType:

let quoteHandler = {
    confirmCreate: function(){
        dialog.showDialog({
            contentTpl: tpl,
            className: "confirm-create-quote"
        });
        let $quoteDialog = $(".confirm-create-quote form")[0];
        return new Promise(resolve => {
            $(form.submit).on("click", function(event){
                resolve(form.createType.value);
            });
        });
    }

}
複製程式碼

這樣外部呼叫者就可以使用await,而不用傳遞一個點選事件的回撥函式了。

但是需要注意的是await的一次性執行特點。相對於回撥函式來說,await的執行是一次性的,例如監聽點選事件,然後使用await,那麼點選事件只會執行一次,因為程式碼從上往下執行完了,所以當希望點選之後出錯了還能繼續修改和提交就不能使用await,另外使用await獲取非同步資料,如果出錯了,那麼成功的resolve就不會執行,後續的程式碼也不會執行,所以請求出錯的時候基本邏輯不會有問題。


要在babel裡面使用await,需要:

(1)安裝一個Node包

npm install --save-dev babel-plugin-transform-async-to-generator

(2)在工程的根目錄新增一個.babelrc檔案,內容為:

{
  "plugins": ["transform-async-to-generator"]
}
複製程式碼

(3)使用的時候先引入一個模組

require("babel-polyfill");複製程式碼

然後就可以愉快地使用ES7的await了。

使用await的函式前面需要加上async關鍵字,如下程式碼:

async showOrderDialog() {
     // 獲取建立型別
     let createType = await quoteHandler.confirmCreate();

     // 獲取老訂單資料 
     let orderInfo = await orderHandler.getOrderData();
}
複製程式碼


我們再舉一個例子:使用await實現JS版的sleep函式,因為原生是沒有提供執行緒休眠函式的,如下程式碼所示:

function sleep (time) {
    return new Promise(resolve => 
                          setTimeout(() => resolve(), time));
}

async function start () {
    await sleep(1000);
}

start();
複製程式碼


babel的await實現是轉成了ES6的generator,如下關鍵程式碼:

while (1) {
    switch (_context.prev = _context.next) {
        case 0:
            _context.next = 2;
            // sleep返回一個Promise物件
            return sleep(1000);

        case 2:
        case "end":     
            return _context.stop();
    }
}複製程式碼

而babel的generator也是要用ES5實現的,什麼是generator呢?如下圖所示:

生成器用function*定義,每次執行生成器的next函式的時候會返回當前生成器裡用yield返回的值,然後生成器的迭代器往後走一步,直到所有yield完了。

有興趣的可以繼續研究babel是如何把ES7轉成ES5的,據說原生的實現還是直接基於Promise.


使用await還有一個好處,可以直接try-catch捕獲非同步過程丟擲的異常,因為我們是不能直接捕獲非同步回撥裡面的異常的,如下程式碼:

let quoteHandler = {
    confirmCreate: function(){
        $(form.submit).on("click", function(event){
            // 這裡會拋undefined異常:訪問了undefined的value屬性
            callback(form.notFoundInput.value);
        });
    }
}

try {
    // 這裡無法捕獲到異常
    quoteHandler.confirmCreate();
} catch (e) {

}複製程式碼

上面的try-catch是沒有辦法捕獲到異常的,因為try裡的程式碼已經執行完了,在它執行的過程中並沒有異常,因此無法在這裡捕獲,如果使用Promise的話一般是使用Promise鏈的catch:

let quoteHandler = {
    confirmCreate: function(){
        return new Promise(resolve => {
            $(form.submit).on("click", function(event){
                // 這裡會拋undefined異常:訪問了undefined的value屬性
                resolve(form.notFoundInput.value);
            });
        });
    }
}

quoteHandler.confirmCreate().then(createType => {

}).catch(e => {
    // 這裡能捕獲異常
});複製程式碼

而使用await,我們可以直接用同步的catch,就好像它真的變成同步執行了:

try {
    createType = await quoteHandler.confirmCreate("order");
}catch(e){
    console.log(e);
    return;
}複製程式碼


總之使用await讓程式碼少寫了很多巢狀,很方便的邏輯處理,縱享絲滑。


相關文章