1. 程式人生 > >理解與使用Promise完成複雜非同步處理流程

理解與使用Promise完成複雜非同步處理流程

本文談到的Promise是指javascript環境下的Promise,然而Promise這個功能在若干語言中均有實現,我本次會在Nodejs服務端環境下進行學習和理解。

Promise是為了解決日趨複雜的非同步程式設計而出現的,簡單的非同步例如:發起一個ajax請求來獲取資料,之後渲染DOM。

然而現實世界並沒有這麼簡單,我們極有可能需要同時發起多個ajax請求並等待它們全部返回,在獲得結果後可能又需要根據上一輪的資料發起下一輪的ajax獲取其他資料,這樣的流程完全可以演變得交織錯雜,程式設計和維護起來是非常頭疼的。

此前,我們解決這種問題就是callback的回撥思路,callback巢狀callback的程式碼層出不窮,想追加點功能需要一層一層的數括號,這就急需一個替代方案的出現。

Promise應運而生!

Promise提供了序列/並行非同步程式設計的簡化方案,ajax-2依賴ajax-1,或者ajax3依賴ajax-1+ajax2,都可以輕鬆通過它輕鬆實現。

學習Promise的文章挺多的,我個人感覺沒有說的特別明白的部落格。但是,我建議你先看看這個,然後跟著我下面的例子來理解一下就行。

談談我對Promise的理解

Promise物件有一個狀態,表示非同步處理的一個進度,可以是:pending(等待結果),resolved(已完成),rejected(已失敗);另外還有一個儲存的區域,它用於放置非同步處理完成後(resolved or rejected)的結果資料,可以是任意格式。

既然Promise本身只有這個狀態而已,那麼顯然需要我們業務程式碼去驅動它的進度變遷,否則它就是一坨不會動的程式碼。這裡,Promise的建構函式需要傳入1個接受2個引數的function,我們一般是這樣用的:

new Promise( /* executor */ function(resolve, reject) { ... } );

這個函式提供了2個function給業務呼叫,呼叫Resolve就可以改變這個Promise的狀態為resolved,同樣道理呼叫reject就可以讓Promise的狀態變為rejected。

resolve()/reject()函式接受1個入參,它會被傳遞給後面串聯的.then()呼叫,這個入參可以是一個普通的物件,也可以是一個Promise物件。重要的是,如果是一個Promise物件,那麼這個後面串聯的then()需要等到這個Promise的狀態等於終結狀態(非pending)後才會被回撥,而then()回撥的傳入值是也就是這個Promise最終resolve/reject傳入的值。

有點繞吧,可以先忘記這一個段落,看看then()做了什麼再回頭理解。

那麼,then()又是做什麼的呢?

其實也很簡單,如果你在上述業務邏輯裡呼叫了resolve,那麼Promise非同步處理相當於終結了,then()就是指前一個Promise終結後再做什麼事情的意思。

then()的函式原型也不復雜,要求我們傳入一個function,當Promise通過resolve(xxx)/reject(xxx)終結後,xxx會被當做入參呼叫這個function,我們可以在裡面做下一步的事情,從而實現串聯的感覺。

值得注意的是,Promise為了實現.then()呼叫的串聯(只有Promise物件有then方法),.then()的回撥函式的返回值會被隱式的轉換為Promise物件(如果你沒有顯式的返回Promise物件),這是then實現內部通過Promise.resolve/Promise.reject這兩個API實現的,我會在後面的例子中體現這個事情。

實踐出真知

為了加深理解,我親手寫了8個demo來看體驗promise的各種特性,我這裡逐一列出來做一個簡短的說明。

如果實在理解有困難可以把程式碼拉到本地,使用node promise.js來執行除錯一下。

// 同步resolve
var promise1 = new Promise(
    (resolve, reject) => {
        resolve("this is promise1 resolve");
    }
).then(
    (msg) => {
        console.log(msg);
    },
    (err) => {
        console.log(err);
    }
);

var promise = 這部分可以無視,我僅僅用於程式碼裡標記一下demo的次序。這個例子體現了最基礎用法,給resolve傳入一個字串終結當前的Promise的狀態,因為Promise被終結,因此該字串會被回撥給then中的(msg) => {...}函式,從而實現串聯。

// 同步reject
var promise2 = new Promise(
    (resolve, reject) => {
        reject("this is promise2 reject");
    }
).then(
    (msg) => {
        console.log(msg);
    },
    (err) => {
        console.log(err);
    }
);

和上個例子差不多,只是呼叫了reject,這樣會回撥(err) => {....}。

// 同步catch
var promise3 = new Promise(
    (resolve, reject) => {
        reject("this is promise3 reject catch");
    }
).then(
    (msg) => {
        console.log(msg);
    }
).catch(
    (err) => {
        console.log(err);
    }
);

如果我沒有在then()裡提供reject的回撥函式,那麼這個reject事件會繼續向後移動,直到遇到catch會被處理。

// 非同步resolve
var promise4 = new Promise(
    (resolve, reject) => {
        var promise4_1 = new Promise(
            (resolve, reject) => {
                console.log("promise4_1 starts");
                setTimeout(
                    () => {
                        resolve("this is promise4_1 resolve");
                    },
                    2000
                );
            }
        );
        resolve(promise4_1);
    }
).then(
    (msg) => {
        console.log(msg);
    },
    (err) => {
        console.log(err);
    }
);

這裡,我故意營造了一個resolve(Promise Object)的例子(也就是promise4_1),這樣的話then()會等到這個Promise Object自身的非同步流程處理結束後再回調,這相當於為promise4非同步流程節外生枝了promise4_1,等枝葉長成後再回到promise4主幹繼續向後鏈式處理。

// 鏈式resolve
var promise5 = new Promise(
    (resolve, reject) => {
        var promise4_1 = new Promise(
            (resolve, reject) => {
                console.log("promise5_1 starts");
                setTimeout(
                    () => {
                        resolve("this is promise5_1 resolve");
                    },
                    2000
                );
            }
        );
        resolve(promise4_1);
    }
).then(
    (msg) => {
        console.log(msg);
        var promise5_2 =  new Promise(
            (resolve, reject) => {
                console.log("promise5_2 starts");
                setTimeout(
                    () => {
                        resolve("this is promise5_2 resolve");
                    },
                    2000
                );
            }
        );
        return promise5_2;
    }
).then(
    (msg) => {
        console.log(msg);
        throw new Error();
    }
).catch(
    () => {
        console.log("exception catched after promise5_2 resolved");
    }
);

這個例子變得再複雜一些,除了在promise5中節外生枝promise5_1非同步處理2秒,在2秒後回到主幹後的.then()環節,我通過return返回一個Promise物件再次節外生枝promise5_2非同步執行2秒,之後再次回到主幹的.then()打印出訊息並且丟擲了異常,最終由catch捕獲。

// 並行+鏈式promise
var promise6 = new Promise(
    (resolve, reject) => {
        var promiseArr = [];
        for (var i = 0; i < 5; ++i) {
            promiseArr.push(new Promise(
                (resolve, reject) => {
                    console.log(`promise6_${i} starts`);
                    ((index) => { // 閉包處理i
                        setTimeout(
                            () => {
                                console.log(`before promise6_${index} resolved`);
                                resolve(`this is promise6_${index} resolve`);
                            },
                            index * 1000
                        );
                    })(i);
                }
            ));
        }
        resolve(Promise.all(promiseArr));
    }
).then(
    (msgArr) => {
        console.log(`promise6 all resolved ${msgArr}`);
    }
);

這個例子主要是體驗Promise.all(),這個函式其實建立返回了一個Promise物件,內部管理與併發了多個Promise流程(節外生枝了N個樹叉),它等待它們全部完成或者任意失敗之後會終結自己,在外層通過resolve將Promise.all()返回的集合式Promise物件串聯(託管)起來,最終進入下一個then從而可以訪問N個樹叉的結果集合。

// .then()隱式包裝resolved Promise
var promise7 = new Promise(
    (resolve, reject) => {
        var promise7_1 = new Promise(
            (resolve, reject) => {
                console.log("promise7_1 starts");
                setTimeout(
                    () => {
                        resolve("this is promise7_1 resolve");
                    },
                    2000
                );
            }
        );
        resolve(promise7_1);
    }
).then(
    (msg) => {
        console.log(msg);
        return "promise7 .then()隱式包裝resolved Promise";
    },
    (err) => {
        console.log(err);
    }
).then(
    (word) => {
        console.log(word);
    }
);

這個例子除了節外生枝外,主要關注在於第1個.then()中return了一個字串,它實際被隱式的包裝成了一個resolved狀態的Promise物件返回(這是我想強調的重點),從而繼續鏈式的呼叫第2個.then()的(word) => {...}回撥函式。

// .then()顯式包裝resolved Promise
var promise8 = new Promise(
    (resolve, reject) => {
        var promise8_1 = new Promise(
            (resolve, reject) => {
                console.log("promise8_1 starts");
                setTimeout(
                    () => {
                        resolve("this is promise8_1 resolve");
                    },
                    2000
                );
            }
        );
        resolve(promise8_1);
    }
).then(
    (msg) => {
        console.log(msg);
        return Promise.resolve("promise8 .then()顯式包裝resolved Promise");
    },
    (err) => {
        console.log(err);
    }
).then(
    (word) => {
        console.log(word);
    }
);

這個例子和上一個例子等價,這裡體現了第1個.then()顯式呼叫Promise.resolve返回一個Promise物件,從而第2個.then()回撥(word) => {}。

// .then()顯式包裝rejected Promise
var promise9 = new Promise(
    (resolve, reject) => {
        var promise9_1 = new Promise(
            (resolve, reject) => {
                console.log("promise9_1 starts");
                setTimeout(
                    () => {
                        resolve("this is promise9_1 resolve");
                    },
                    2000
                );
            }
        );
        resolve(promise9_1);
    }
).then(
    (msg) => {
        console.log(msg);
        return Promise.reject("promise9 .then()顯式包裝rejected Promise");
    },
    (err) => {
        console.log(err);
    }
).catch(
    (word) => {
        console.log(word);
    }
);

這個例子和上面2個例子相反,我在第1個.then()顯式的返回了一個rejected的Promise物件,這是通過Promise.reject包裝字串而成的,因此catch將被呼叫。

通過最後3個例子,我們應該可以明確的感受到Promise圍繞pending,resolved,rejected三個狀態實現的非同步狀態驅動以及串聯/並行呼叫的觸發動機與原理。

關於Promise本身的功能就瞭解這麼多,希望後面有機會在react下多多使用,解決一些併發ajax以及串聯ajax的非同步需求,關鍵還是找到應用場景進行合理的套用,這是我認為最難的地方。

另外,需要記住Promise是ES6的產物,而未來ES7提出了async/await關鍵字將對Promise加以利用進一步簡化非同步程式設計,它將更接近於協程的理念,更加符合人類的思考習慣,至少我是這麼認為的。