Promise讓程式碼變得更人性化
曾經我一直在思考,為什麼程式碼會比較難讀。後來發現,我們平時要閱讀的所有媒體:報紙、書、新聞,我們在閱讀的時候,都是從上到下一直讀下去的,然而,我們的在讀程式碼的時候,經常要跳著去讀,這種閱讀方式其實是反人類的,如果我們能在讀程式碼的時候,也可以從上往下一直讀下去,那麼,程式碼就會變得可讀性提高很多。
對比JS中,callback是讓我們跳來跳去讀程式碼最大的罪魁禍首,它讓我們的流程變得混亂,Promise正是為了解決這一問題而產生的。我們來對比一下這個過程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
var render = function(data){ }; var getData = function(callback){ $.ajax({ success: function(){ callback(data); } }); }; var init = function(){ getData(function(data){ render(data); getData(function(data){ render(data); }); }); }; init(); |
使用Promise之後
1 2 3 4 5 6 7 8 9 10 11 12 |
var init = function(){ getData({ }).then(function(data){ render(data); return getData({}); }).then(function(data){ render(data); }); }; |
很明顯看出,程式碼就變成線性的了,邏輯也變得更加清晰可讀
Promise流程再優化
promse出來之後,大家都有很多的想法,在Promise之上再封裝一層,使用非同步流程更清晰可讀。下面是Abstract-fence 2.0(開發中)的一種解決方案(規範)
Abstract-fence中,function會被分解為多個task
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Model.task('getData', function(scope, next,{$, util}){ $.ajax({ success: function(data){ next(); } }); }); Model.task('render', ['getData'], function(scope){ var data = scope.data; // 使用data進行渲染 }); Model.task('init', [render].then(render)); Model.runWorkflow(init); |
其中, init是task render執行後再執行render, 而render任務又是由getData任務執行後再渲染組成,其中每個task的定義function的引數使用依賴注入傳遞,全域性屬性使用{}包裹
但是在使用Promise.all的過程中,遇到了一個Promise奇怪的問題
Array.prototype.then與Promise.all
很簡單的一段程式碼
1 2 3 4 5 6 7 8 9 |
var p = new Promise(function(rs, rj){ setTimeout(rs, 1000); }); Promise.all([p]).then(function(){ console.log(2); }); console.log(1); |
毫無疑問,這段程式碼在瀏覽器執行會先列印1,然後再輸出2 但如果在前面增加對then方法的定義,如下程式碼
1 2 3 4 5 6 7 8 9 10 11 |
Array.prototype.then = function(){ }; var p = new Promise(function(rs, rj){ setTimeout(rs, 1000); }); Promise.all([p]).then(function(){ console.log(2); }); console.log(1); |
那麼這段程式碼只會列印出1, 2卻永遠不會執行
查了很多資料,確認promise.all的引數只能接收陣列(類陣列)
比如如下程式碼就會報錯
1 2 3 4 5 |
var p = new Promise(function(rs){}); Promise.all(p); // 報錯 Promise.all([p]); // ok |
所以,Promise.all接收一個Iterator可遍歷物件
對陣列的prototype.then定義為什麼會影響到Promise的行為呢?
Promise A+規範
Promise A+規範看起來還是有點繞,這裡省略掉一些具體的實現細節,將Promise A+更直白的闡述如下
1. Promise then方法需要return一個新的Promise出來,如下
1 2 3 4 5 |
.then = function(rsFunc, rjFunc){ var promise2 = new Promise(); return promise2; }; |
2. 如果promise本身狀態變更到fulfilled之後,呼叫rsFunc,rsFunc的解析值x, 與新的promise2進行promise的解析過程[[Resolve]](promise2, x), x的取值不同,有不同的情況
3. 若x為一個promise,則x完成的最後,再fufill promise2, 對應如下程式碼
1 2 3 4 5 6 7 8 |
new Promise(function(rs, rj){ rs(); }).then(function(data){ // 對應於x的返回值 return new Promise(rs, rj){ }); }); |
4. 若x為一個物件或者函式,如果有then方法,將會執行then方法,then方法this指向x本身,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
new Promise(function(rs, rj){ rs(); }).then(function(data){ // 對應於x的返回值 return { a: 1, then: function(rs, rj){ console.log(this.a); rs({a:2}); } }; }).then(function(data){ console.log(data.a); }); |
5. 如果x沒有then方法,那麼,x將會做為值來 滿足 promise2
1 2 3 4 5 6 7 8 9 10 11 |
new Promise(function(rs, rj){ rs(); }).then(function(data){ // 對應於x的返回值 return { a: 1 }; }).then(function(data){ console.log(data.a); }); |
Promise A+給出了一些具體的執行細節,保證了then的順序執行,但在規範中,並未提到Promise.all方法的執行方式
為此,檢視bluebird的Promise.all實現方法
BlueBird關於Promise.all實現方法解析
首先,promise中引用promise_array程式碼如下(已略去一些無關程式碼)
1 2 3 4 5 6 7 8 9 10 |
var INTERNAL = function(){}; var apiRejection = function(msg) { return Promise.reject(new TypeError(msg)); }; function Proxyable() {} var tryConvertToPromise = require("./thenables")(Promise, INTERNAL); var PromiseArray = require("./promise_array")(Promise, INTERNAL, tryConvertToPromise, apiRejection, Proxyable); |
promise.all的實現也很簡單
1 2 3 |
Promise.all = function (promises) { return new PromiseArray(promises).promise(); }; |
可見,具體的細節在promise_array中的實現
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function PromiseArray(values) { var promise = this._promise = new Promise(INTERNAL); if (values instanceof Promise) { promise._propagateFrom(values, 3); } promise._setOnCancel(this); this._values = values; this._length = 0; this._totalResolved = 0; // 初始化 this._init(undefined, -2); } |
PromiseArray的構造方法中,將引數賦值給this._values,待_init方法中使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
PromiseArray.prototype._init = function init(_, resolveValueIfEmpty) { var values = tryConvertToPromise(this._values, this._promise); // 如果values可以轉化為promise物件,那麼根據不同的狀態,會提前return if (values instanceof Promise) { values = values._target(); var bitField = values._bitField; ; this._values = values; // 這個狀態是pending的狀態 if (((bitField & 50397184) === 0)) { this._promise._setAsyncGuaranteed(); // Promise,將會等Promise物件本身狀態改變後再次 return values._then( init, this._reject, undefined, this, resolveValueIfEmpty ); // FULFILLED, 並沒有提前return, 繼續進行 } else if (((bitField & 33554432) !== 0)) { values = values._value(); // rejected的狀態,提前終止 } else if (((bitField & 16777216) !== 0)) { return this._reject(values._reason()); } else { return this._cancel(); } } values = util.asArray(values); if (values === null) { var err = apiRejection( "expecting an array or an iterable object but got " + util.classString(values)).reason(); this._promise._rejectCallback(err, false); return; } if (values.length === 0) { if (resolveValueIfEmpty === -5) { this._resolveEmptyArray(); } else { this._resolve(toResolutionValue(resolveValueIfEmpty)); } return; } this._iterate(values); }; |
init總結為幾步
1.嘗試轉換引數為Promise物件
2.如果轉換成功,那麼檢查Promise物件的狀態
1. Pending,等待Promise
2. fulfilled, 換取返回值,繼續進行
3. Rejected 終止,返回原因
4. 其他, 終止
上面的程式碼可以看出,一旦陣列的具有then方法,就可被tryConvertToPromise方法轉換為一個Promise物件,如果then方法未實現promise規範,那麼Promise物件就會處於Pending的狀態,Promise.all方法永遠就不會達到fulfilled的條件,問題也就明白了