值得多聊聊的 Promise 模式,以及它能解決什么問題
Promise 模式是解決 Callback hell 的一個很好的方式, 值得我們再多聊一聊。
什么是 Callback Hell
所謂 Callback Hell,就是我們熟悉的閉包或者說是回調函數,連續多級嵌套,導致代碼結構的混亂。 比如這樣:
step1(function(value1){
step2(value1, function(value2){
step3(value2, function(value3){
step4(value3, function(value4){
// Do something with value4
});
});
});
});
這些 callback 的嵌套層級過多的話,會導致代碼結構上很容易出錯,不易閱讀和維護。 甚至有一個網站專門介紹它: http://callbackhell.com/ 感興趣的同學可以了解一下。
Promise 模式
基于 callback 的這個問題,業界也提出了很多解決方案,其中一個就是 Promise 模式。 關于這個模式,之前的文章中也給大家介紹過。這次更多跟大家要聊的,是一個基于 Promise 模式的實現庫 - Q。 它提供了更多完善的接口。
比如:
Q.fcall(function(){
return 2;
}).then(function(val){
console.log(val);
//...
}).then(function(){
//...
}).done();
Q.fcall 是整個 Promise 鏈條的入口,是我們調用的第一個函數。 然后一系列的 then 調用,會傳入接下來的回調函數。 前一個的返回值可以作為后一個調用的參數。 這樣就可以把前面多層嵌套的 callback 結構變成線性的 Promise 結構。
Promise 除了改變調用的語法結構之外,還有什么其他的收益呢。答案是肯定的。 假如我們要寫一個單元測試,用于測試數據庫訪問接口。 我們需要預先插入一系列測試數據, 但數據庫 API 的調用需要異步 callback, 如果沒有 Promise, 我們可能就會寫出這樣的代碼:
dao.add({"title" : "t1" }, function(success){
dao.add({"title" : "t2" }, function(success){
dao.add({"title" : "t3" }, function(success){
done();
}
}
});
這還是指插入 3 條測試數據,就已經如此復雜,如果我們要插入幾十條數據,這樣的語法結構基本無法滿足需求。 有人說了,我們可不可以這樣并行的寫呢:
dao.add({"title" : "t1" }, function(success){
});
dao.add({"title" : "t2" }, function(success){
});
dao.add({"title" : "t3" }, function(success){
});
答案是不行, 因為這個方法是異步執行的, 我們需要等待所有的添加操作都完成,才能進行下一步操作,也就是必須等待 callback 調用,在 callback 中執行下一步操作。 所以這個寫法會導致我們程序的邏輯錯誤。 再來看看使用 Promise 怎么解決這個問題:
var testData = [];
//準備測試數據
for(var i = 0;i < 50;i++) {
testData.push(function(){
var deferred = Q.defer();
var info = mockImageInfo();
dao.add(info, function(){
deferred.resolve("");
});
return deferred.promise;
});
}
testData.reduce(Q.when, Q("")).then(function(){
//數據插入完成后,進行操作
}).done();
上面的代碼一眼看上去是不是有些不好理解。這里給大家講解一下, 首先我們開始的 for 循環會創建 50 個函數,每個函數都會返回一個 Promise 實例 - return deferred.promise 。 每個函數做的事情就是調用異步方法添加測試數據。
Q 這個庫提供了對異步函數的 Promise 支持, 首先調用 Q.defer() , 然后在異步函數的 callback 方法中調用 deferred.resolve 用于標示這個 Promise 執行成功。 真個函數的邏輯是這樣, 我們先調用 dao.add 方法異步添加數據,然后立即調用 return deferred.promise 返回 Promise 實例。 因為這個時候 Promise 具體狀態還沒有確認,要等到異步方法執行完畢, 調用 deferred.resolve 確認狀態后,這時整個 Promise 就算執行完成了。
我們創建好這 50 個函數后, 怎么保證他們順序執行呢? 接下來調用 testData.reduce 方法, 這個方法是 JS 對 Array 類型提供的內建方法,它的作用就是遍歷整個數組, 對每個元素執行一個操作函數, 我們這里是 Q.when, 用于處理數組中所有 Promise 函數的執行。
reduce 方法調用完成后,會保證所有 50 個測試數據都插入到數據庫中, 然后調用 then 方法,執行下一步的相關操作。 這時所有 50 個異步調用都確保執行完畢。 想一下如果用我們之前的嵌套是寫法插入這 50 個數據,那簡直就是噩夢了。 Promise 這種方式我們可以插入任意數量的數據,不會影響代碼結構。
Q 的 Github 地址: https://github.com/kriskowal/q
總結
這次文章中和大家重新熟悉了一下 Promise 模式以及它解決的問題, 還給大家介紹了一個實現庫 - Q。 另外呢也結合我實際開發過程中給大家列舉了一個應用場景。 也是我在給一個工程寫單元測試的時候,遇到的一個實際問題。相信通過這個實際的問題,能幫助大家更好的理解 Promise 的應用場景。
如果你覺得這篇文章有幫助,還可以關注微信公眾號 swift-cafe,會有更多我的原創內容分享給你~
來自:http://www.swiftcafe.io/2017/05/24/promise-q/