閒話Promise機制

  Promise的誕生與Javascript中異步編程息息相關,js中異步編程主要指的是setTimout/setInterval、DOM事件機制、ajax,經過傳入回調函數實現控制反轉。異步編程爲js帶來強大靈活性的同時,也帶來了嵌套回調的問題。詳細來講主要有兩點,第一嵌套太深代碼可讀性太差,第二並行邏輯必須串行執行。javascript

 1 request = function(url, cb, eb) {
 2     var xhr = new XMLHttpRequest();
 3     xhr.onreadystatechange = function() {
 4         if (xhr.readyState === 4) {
 5             if ((xhr.status >=200 && xhr.status < 300) || xhr.status === 304) {
 6                 cb(xhr.responseText);
 7             } else {
 8                 eb(new Error({
 9                     message: xhr.status
10                 }));
11             }
12         }
13     };
14     xhr.open('get', url, true);
15     xhr.send(null);
16 }

  這個例子中程序要依次處理data一、data二、data3,嵌套太多可讀性太差html

 1 //回調函數嵌套過深
 2 request('data1.json', function(data1){
 3     console.log(data1);//處理data1
 4     request('data2.json', function(data2) {
 5         console.log(data2);//處理data2
 6         request('data3.json', function(data3) {
 7             console.log(data3);//處理data3
 8 
 9             alert('success');
10         }, function(err) {
11             console.error(err);
12         });
13     }, function(err) {
14         console.error(err);
15     });
16 }, function(err) {
17     console.error(err);
18 });

  這個例子中程序須要請求data一、data二、data3數據,獲得三個數據後才進行下一步處理。數據並不須要串行請求,但咱們的代碼卻須要串行執行,增長了等待時間。前端

 1 //並行邏輯串行執行
 2 request('data1', function(data1) {
 3     request('data2', function(data2) {
 4         request('data3', function(data3) {
 5             console.log(data1, data2, data3);//處理所有數據
 6 
 7             alert('success');
 8         }, function(err) {
 9             console.error(err);
10         });
11     }, function(err) {
12         console.error(err);
13     });
14 }, function(err) {
15     console.error(err);
16 });

 

Promise機制java

  Promise機制即是上述問題的一種解決方案。與他相關的規範有PromiseAPromiseA+,PromiseA中對Promise進行了總體描述,PromiseA+對A進行了補充,在then函數的行爲方面進行了更加詳盡的闡述。ajax

PromiseA+規範
promise represents the eventual result of an asynchronous operation. 
一個promise表明了一個異步操做的最終結果
The primary way of interacting with a promise is through its then method, which registers callbacks to receive either a promise’s eventual value or the reason why the promise cannot be fulfilled.跟promise交互的主要方式是經過他的then方法來註冊回調函數去接收promise的最終結果值或者是promise不能完成的緣由。

  咱們能夠簡單總結一下規範。每一個promise都有三個狀態:pending(默認)、fulfilled(完成)、rejected(失敗);默認狀態能夠轉變爲完成態或失敗態,完成態與失敗態之間沒法相互轉換,轉變的過程是不可逆的,轉變一旦完成promise對象就不能被修改。經過promise提供的then函數註冊onFulfill(成功回調)、onReject(失敗回調)、onProgres(進度回調)來與promise交互。Then函數返回一個promise對象(稱爲promise2,前者成爲promise1),promise2受promise1狀態的影響,具體請查看A+規範。編程

  上兩個規範中並無說明promise的狀態如何改變,大部分前端框架中使用Deferred來改變promise的狀態(resolve()、reject())。兩者關係請看下圖。json

  這裏根據規範,咱們實現一下promise數組

  1 Promise = function() {
  2     this.queue = [];
  3     this.value = null;
  4     this.status = 'pending';// pending fulfilled rejected
  5 };
  6 
  7 Promise.prototype.getQueue = function() {
  8     return this.queue;
  9 };
 10 Promise.prototype.getStatus = function() {
 11     return this.status;
 12 };
 13 Promise.prototype.setStatus = function(s, value) {
 14     if (s === 'fulfilled' || s === 'rejected') {
 15         this.status = s;
 16         this.value = value || null;
 17         this.queue = [];
 18         var freezeObject = Object.freeze || function(){};
 19         freezeObject(this);// promise的狀態是不可逆的
 20     } else {
 21         throw new Error({
 22             message: "doesn't support status: " + s
 23         });
 24     }
 25 };
 26 Promise.prototype.isFulfilled = function() {
 27     return this.status === 'fulfilled';
 28 };
 29 Promise.prototype.isRejected = function() {
 30     return this.status === 'rejected';
 31 }
 32 Promise.prototype.isPending = function() {
 33     return this.status === 'pending';
 34 }
 35 Promise.prototype.then = function(onFulfilled, onRejected) {
 36     var handler = {
 37         'fulfilled': onFulfilled,
 38         'rejected': onRejected
 39     };
 40     handler.deferred = new Deferred();
 41 
 42     if (!this.isPending()) {//這裏容許先改變promise狀態後添加回調
 43         utils.procedure(this.status, handler, this.value);
 44     } else {
 45         this.queue.push(handler);//then may be called multiple times on the same promise;規範2.2.6
 46     }
 47     return handler.deferred.promise;//then must return a promise;規範2.2.7
 48 };
 49 
 50 var utils = (function(){
 51     var makeSignaler = function(deferred, type) {
 52         return function(result) {
 53             transition(deferred, type, result);
 54         }
 55     };
 56 
 57     var procedure = function(type, handler, result) {
 58         var func = handler[type];
 59         var def = handler.deferred;
 60 
 61         if (func) {
 62             try {
 63                 var newResult = func(result);
 64                 if (newResult && typeof newResult.then === 'function') {//thenable
 65                     // 此種寫法存在閉包容易形成內存泄露,咱們經過高階函數解決
 66                     // newResult.then(function(data) {
 67                     //     def.resolve(data);
 68                     // }, function(err) {
 69                     //     def.reject(err);
 70                     // });
 71                     //PromiseA+規範,x表明newResult,promise表明def.promise
 72                     //If x is a promise, adopt its state [3.4]:
 73                     //If x is pending, promise must remain pending until x is fulfilled or rejected.
 74                     //If/when x is fulfilled, fulfill promise with the same value.
 75                     //If/when x is rejected, reject promise with the same reason.
 76                     newResult.then(makeSignaler(def, 'fulfilled'), makeSignaler(def, 'rejected'));//此處的本質是利用了異步閉包
 77                 } else {
 78                     transition(def, type, newResult);
 79                 }
 80             } catch(err) {
 81                 transition(def, 'rejected', err);
 82             }
 83         } else {
 84             transition(def, type, result);
 85         }
 86     };
 87 
 88     var transition = function(deferred, type, result) {
 89         if (type === 'fulfilled') {
 90             deferred.resolve(result);
 91         } else if (type === 'rejected') {
 92             deferred.reject(result);
 93         } else if (type !== 'pending') {
 94             throw new Error({
 95                 'message': "doesn't support type: " + type
 96             });
 97         }
 98     };
 99 
100     return {
101         'procedure': procedure
102     }
103 })();
104 
105 Deferred = function() {
106     this.promise = new Promise();
107 };
108 
109 Deferred.prototype.resolve = function(result) {
110     if (!this.promise.isPending()) {
111         return;
112     }
113 
114     var queue = this.promise.getQueue();
115     for (var i = 0, len = queue.length; i < len; i++) {
116         utils.procedure('fulfilled', queue[i], result);
117     }
118     this.promise.setStatus('fulfilled', result);
119 };
120 
121 Deferred.prototype.reject = function(err) {
122     if (!this.promise.isPending()) {
123         return;
124     }
125 
126     var queue = this.promise.getQueue();
127     for (var i = 0, len = queue.length; i < len; i++) {
128         utils.procedure('rejected', queue[i], err);
129     }
130     this.promise.setStatus('rejected', err);
131 }

  經過Promise機制咱們的編程方式能夠變成這樣:promise

 1 request = function(url) {
 2     var def = new Deferred();
 3 
 4     var xhr = new XMLHttpRequest();
 5     xhr.onreadystatechange = function() {
 6         if (xhr.readyState === 4) {
 7             if ((xhr.status >=200 && xhr.status < 300) || xhr.status === 304) {
 8                 def.resolve(xhr.responseText)
 9             } else {//簡化ajax,沒有提供錯誤回調
10                 def.reject(new Error({
11                     message: xhr.status
12                 }));
13             }
14         }
15     };
16     xhr.open('get', url, true);
17     xhr.send(null);
18 
19     return def.promise;
20 }
21 
22 request('data1.json').then(function(data1) {
23     console.log(data1);//處理data1
24     return request('data2.json');
25 }).then(function(data2) {
26     console.log(data2);//處理data2
27     return request('data3.json');
28 }, function(err) {
29     console.error(err);
30 }).then(function(data3) {
31     console.log(data3);
32     alert('success');
33 }, function(err) {
34     console.error(err);
35 });

  對於並行邏輯串行執行問題咱們能夠這樣解決ruby

 1 //全部異步操做都完成時,進入完成態,
 2 //其中一項異步操做失敗則進入失敗態
 3 all = function(requestArray) {
 4     // var some = Array.prototype.some;
 5     var def = new Deferred();
 6     var results = [];
 7     var total = 0;
 8     requestArray.some(function(r, idx) {
 9         //爲數組中每一項註冊回調函數
10         r.then(function(data) {
11             if (def.promise.isPending()) {
12                 total++;
13                 results[idx] = data;
14 
15                 if (total === requestArray.length) {
16                     def.resolve(results);
17                 }
18             }
19         },  function(err) {
20             def.reject(err);
21         });
22         //若是不是等待狀態則中止,好比requestArray[0]失敗的話,剩下數組則不用繼續註冊
23         return !def.promise.isPending();
24     });
25 
26     return def.promise;
27 }
28 
29 all(
30     [request('data1.json'),
31     request('data2.json'),
32     request('data3.json')]
33     ).then(
34         function(results){
35             console.log(results);// 處理data1,data2,data3
36             alert('success');
37     }, function(err) {
38         console.error(err);
39     });

  如下是幾個測試案例

  1 //鏈式調用
  2 var p1 = new Deferred();
  3 p1.promise.then(function(result) {
  4     console.log('resolve: ', result);
  5     return result;
  6 }, function(err) {
  7     console.log('reject: ', err);
  8     return err;
  9 }).then(function(result) {
 10     console.log('resolve2: ', result);
 11     return result;
 12 }, function(err) {
 13     console.log('reject2: ', err);
 14     return err;
 15 }).then(function(result) {
 16     console.log('resolve3: ', result);
 17     return result;
 18 }, function(err) {
 19     console.log('reject3: ', err);
 20     return err;
 21 });
 22 p1.resolve('success');
 23 //p1.reject('failed');
 24 p1.promise.then(function(result) {
 25     console.log('after resolve: ', result);
 26     return result;
 27 }, function(err) {
 28     console.log('after reject: ', err);
 29     return err;
 30 }).then(function(result) {
 31     console.log('after resolve2: ', result);
 32     return result;
 33 }, function(err) {
 34     console.log('after reject2: ', err);
 35     return err;
 36 }).then(function(result) {
 37     console.log('after resolve2: ', result);
 38     return result;
 39 }, function(err) {
 40     console.log('after reject2: ', err);
 41     return err;
 42 });
 43 
 44 //串行異步
 45 var p2 = new Deferred();
 46 p2.promise.then(function(result) {
 47     var def = new Deferred();
 48     setTimeout(function(){
 49         console.log('resolve: ', result);
 50         def.resolve(result);
 51     })
 52     return def.promise;
 53 }, function(err) {
 54     console.log('reject: ', err);
 55     return err;
 56 }).then(function(result) {
 57     var def = new Deferred();
 58     setTimeout(function(){
 59         console.log('resolve2: ', result);
 60         def.reject(result);
 61     })
 62     return def.promise;
 63 }, function(err) {
 64     console.log('reject2: ', err);
 65     return err;
 66 }).then(function(result) {
 67     console.log('resolve3: ', result);
 68     return result;
 69 }, function(err) {
 70     console.log('reject3: ', err);
 71     return err;
 72 });
 73 p2.resolve('success');
 74 
 75 //並行異步
 76 var p1 = function(){
 77     var def = new Deferred();
 78     setTimeout(function() {
 79         console.log('p1 success');
 80         def.resolve('p1 success');
 81     }, 20);
 82 
 83     return def.promise;
 84 }
 85 var p2 = function(){
 86     var def = new Deferred();
 87     setTimeout(function() {
 88         console.log('p2 failed');
 89         def.reject('p2 failed');
 90     }, 10);
 91 
 92     return def.promise;
 93 }
 94 
 95 var p3 = function(){
 96     var def = new Deferred();
 97     setTimeout(function() {
 98         console.log('p3 success');
 99         def.resolve('p3 success');
100     }, 15);
101 
102     return def.promise;
103 }
104 
105 all([p1(), p2(), p3()]).then(function(results) {
106     console.log(results);
107 }, function(err) {
108     console.error(err);
109 });
View Code

  

Promise優勢

對比使用Promise先後咱們能夠發現,傳統異步編程經過嵌套回調函數的方式,等待異步操做結束後再執行下一步操做。過多的嵌套致使意大利麪條式的代碼,可讀性差、耦合度高、擴展性低。經過Promise機制,扁平化的代碼機構,大大提升了代碼可讀性;用同步編程的方式來編寫異步代碼,保存線性的代碼邏輯,極大的下降了代碼耦合性而提升了程序的可擴展性。

 

Note:下圖是我整理的dojo/Deferred模塊的脈絡圖,使用dojo的道友能夠看一下

 

參考文章:

javascript 異步編程

jQuery 2.0.3 源碼分析 Deferred(最細的實現剖析,帶圖)

JavaScript異步編程原理

相關文章
相關標籤/搜索