jQuery使用():Deferred有狀態的回調列表(含源碼)

  • deferred的功能及其使用
  • deferred的實現原理及模擬源碼
  • deferred.then方法的實現原理及模擬源碼

 1、deferred的功能及其使用

deferred的底層是基於callbacks實現的,建議再熟悉callbacks的內部機制前提下閱讀這篇博客,若是須要了解callbacks能夠參考:jQuery使用():Callbacks回調函數列表之異步編程(含源碼分析)html

  • deferred.done() 向成功狀態的回調函數列表中添加回調方法
  • deferred.fail() 向失敗狀態的回調函數列表中添加回調方法
  • deferred.progress() 向正在進行狀態的回調函數列表中添加回調方法
  • deferred.resolve() 觸發成功狀態的回調函數列表全部方法
  • deferred.reject() 觸發失敗狀態的回調函數列表全部方法
  • deferred.notify() 觸發正在進行狀態的回調函數列表(當resolve或reject被觸發後就不能觸發notify)

本質上done、fail、progress實質上分別指向了緩存三種狀態的回調對象的add方法,統一由deferred方法引用管理,resolve、reject、notify同理分別指向了三個狀態的回調對象的fire方法(內部實現指向fireWith,爲了設置this指向deferred或者promise)。下面使用這六個方法來實現一個模擬異步狀態回調事件:ajax

 1 var df = $.Deferred();
 2 //註冊成功的回調函數
 3 df.done(function(a){
 4     console.log('ho yeah I do it!' + a);
 5 });
 6 //註冊失敗的回調函數
 7 df.fail(function(a){
 8     console.log('sorry I am loser...' + a);
 9 });
10 //註冊進行時的函數
11 df.progress(function(a){
12     console.log("waiting???" + a);
13 });
14 //用定時器模擬一個異步狀態回調事件 >60 表示成功 ; < 50表示失敗 ; 60><50表示正在進行
15 setInterval(function(){
16     var score = Math.random() * 100;
17     if(score > 60){
18         df.resolve("simpleness");
19     }else if(score < 50){
20         df.reject("difficult");
21     }else{
22         df.notify('be surprised to be dumb');
23     }
24 },1000);

經過deferred對象同時管理三個回調對象,讓代碼語義化實現的更優雅,同時也下降了代碼的冗餘。添加回調函數的形式於callback。add方法一致,能夠實現多個同時添加。可是deferred作的遠遠不止這些,因爲deferred對象是同時存在添加和執行兩種方法,爲了保證調用只能在特定位置觸發,deferred還實現了promise對象只用來實現註冊方法,promis對象上只有指向回調對象add的done、fail、progress方法,以及一個then用來更便捷的添加回調函數的方法。編程

因此上面的方法能夠修改成:設計模式

 1 //用定時器模擬一個異步狀態回調事件 >60 表示成功 ; < 50表示失敗 ; 60><50表示正在進行
 2 function createScore(){
 3     var df = $.Deferred();
 4     setInterval(function(){
 5         var score = Math.random() * 100;
 6         if(score > 60){
 7             df.resolve("simpleness");
 8         }else if(score < 50){
 9             df.reject("difficult");
10         }else{
11             df.notify('be surprised to be dumb');
12         }
13     },1000);
14     return df.promise();
15 }
16 var pom = createScore();
17 //註冊成功的回調函數
18 pom.done(function(a){
19     console.log('ho yeah I do it!' + a);
20 });
21 //註冊失敗的回調函數
22 pom.fail(function(a){
23     console.log('sorry I am loser...' + a);
24 });
25 //註冊進行時的函數
26 pom.progress(function(a){
27     console.log("waiting???" + a);
28 });

上面的代碼修改後,就是一個盜版的ajax的事件反饋機制,在jQuery.ajax中源碼就是經過deferred的異步隊列來實現的。前面還有提到then更便捷的回調註冊方法又是什麼呢?下面來看經過then方法改造上面的代碼:數組

//上面的代碼18~28行能夠採用這段代碼替換
pom.then(function(a){
    console.log('ho yeah I do it!' + a);
},function(a){
    console.log('sorry I am loser...' + a);
},function(a){
    console.log("waiting???" + a);
});

這個模擬示例能夠徹底採用這兩種代碼任意一種實現,那這兩種代碼存在什麼區別呢?區別就是then方法能夠傳入三個參數,分別對應的是done、fail、progress方法的函數註冊,可是then不能給同一個狀態註冊多個回調函數,而done、fail、progress能夠像callback.add()那樣同時註冊多個函數,由於done、fail、progress自己就是指向add()別稱。可是then方法還有另外一個功能就是能連續註冊來替代這種缺陷,而且還能夠接收來自上一個方法的返回值做爲參數:promise

 1 pom.then(function(a){
 2     console.log('ho yeah I do it!' + a);
 3     return "oK"
 4 },function(a){
 5     console.log('sorry I am loser...' + a);
 6     return "no"
 7 },function(a){
 8     console.log("waiting???" + a);
 9     return "why"
10 }).then(function(param){
11     console.log(param);//oK
12 },function(param){
13     console.log(param);//no
14 },function(param){
15     console.log(param);//why
16 });

可是須要注意的是,返回值不能是新的deferred對象,若是是一個新的異步延遲對象返回,後面繼續使用then方法就是做用在新的異步延遲對象上。緩存

 2、deferred的實現原理及模擬源碼

這部分源碼是基於jQuery使用():Callbacks回調函數列表之異步編程(含源碼分析)的模擬回調函數列表對象實現的,沒有測試jQuery的Callbacks對象,代碼暫時實現了promise()方法,then()方法沒有實現,今天有事,有時間再來添加(2019.9.18添加then方法的實現原理分析及模擬源碼)。app

 1 function clone(origin, target){
 2     for(var ele in origin){
 3         target[ele] = origin[ele];
 4     }
 5     return target;
 6 }        
 7 function Deferred(fuc){
 8     //異步延遲對象
 9     var deferred = {}
10     var tuples = [
11         ["resolve","done",Callback("once memory")],
12         ["reject","fail",Callback("once memory")],
13         ["notify","progress",Callback("memory")]
14     ];
15     var statesum = true;
16     //異步延遲對象的註冊對象
17     var promise = {
18         //返回deferred的promise註冊對象   
19         //源碼中有obj的合併採用extend實現,這裏寫了一個簡單的克隆方法
20         promise:function(obj){
21             return obj != null ? clone(promise,obj) : promise;
22         }
23     }
24     var pending = true;
25     for(var tuple in tuples){
26         var list = tuples[tuple][2];
27         //添加deferred的回調函數註冊方法 done fail progress
28         promise[tuples[tuple][1]] = list.add;
29         //添加deferred的回調函數觸發執行方法 resolve reject ontify
30         deferred[tuples[tuple][0]] = (function(i,obj){
31             return function(){
32                 if(pending){
33                     // console.log(this.state);
34                     deferred[tuples[i][0]+"With"](obj === deferred ? obj : promise,arguments);
35                     tuples[i][0] == "resolve" || tuples[i][0] == "reject" ? pending = false : "";
36                 }
37                 return obj;
38             }
39         })(tuple,this);
40         deferred[tuples[tuple][0] + "With"] = list.fireWith;
41     }
42     //將promise合併到deferred上 -- 一樣採用克隆方法實現
43     promise.promise(deferred);
44     //fcn執行 上下文指向deferred  參數設置爲deferred
45     if(fuc){
46         fuc.call(deferred,deferred);
47     }
48     return deferred;
49 }

 3、deferred.then方法實現原理及模擬源碼

1.deferred.then方法仍是基於promise實現的,而後被克隆到deferred對象上。在jQuery源碼中考慮內容篇幅和分析目標是異步回調的實現邏輯,不對promise的具體實現目的分析,這部分會在ES6的promise對象中會具體解析,下面先來看看deferred.then方法具體能實現什麼?dom

2.deferred.then方法的語法:異步

1 方法一:
2 deferred.then( doneFilter [, failFilter ] [, progressFilter ] ) 
3 方法二:
4 deferred.then( doneCallbacks, failCallbacks [, progressCallbacks ] ) 

以上兩種參數的傳入實際上是一種傳參,只是第二個方式告訴咱們能夠傳入回調函數列表的觸發函數,本質傳入的三個參數都要是函數類型。先來看一下下面這個示例代碼:

 1 // deferred測試採用jQuery源碼測試
 2 function ddd(){
 3     var df = $.Deferred();
 4     setInterval(function(){
 5         var score = Math.random() * 100;
 6         if(score > 60){
 7             df.resolve("simpleness");
 8         }else if(score < 50){
 9             df.reject("difficult");
10         }else{
11             df.notify('be surprised to be dumb');
12         }
13     },1000);
14     return df.promise();
15 }
16 var pom = ddd();
17 pom.then(function(a){
18     console.log('ho yeah I do it!' + a);
19     return "oK"
20 },function(a){
21     console.log('sorry I am loser...' + a);
22     return "no"
23 },function(a){
24     console.log("waiting???" + a);
25     return "why"
26 }).then(function(param){
27     console.log(param);//oK
28 },function(param){
29     console.log(param);//no
30 },function(param){
31     console.log(param);//why
32 });

前面說過也能夠傳入回調函數列表的觸發方法,用下面這個改造的示例代碼來測試:

 1 // deferred測試
 2 function ddd(){
 3     var df = $.Deferred();
 4     setInterval(function(){
 5         var score = Math.random() * 100;
 6         if(score > 60){
 7             df.resolve("simpleness");
 8         }else if(score < 50){
 9             df.reject("difficult");
10         }else{
11             df.notify('be surprised to be dumb');
12         }
13     },1000);
14     return df.promise();
15 }
16 var pom = ddd();
17 var resolveCallback = $.Callbacks();
18 var rejectCallback = $.Callbacks();
19 var notifyCallback = $.Callbacks();
20 
21 resolveCallback.add(function(a){
22     console.log(a + ":1");
23 },function(a){
24     console.log(a + ":2");
25 },function(a){
26     console.log(a + ":3");
27 });
28 rejectCallback.add(function(a){
29     console.log(a + ":1");
30 },function(a){
31     console.log(a + ":2");
32 },function(a){
33     console.log(a + ":3");
34 });
35 notifyCallback.add(function(a){
36     console.log(a + "繼續");
37 });
38 
39 var pom1 = pom.then(resolveCallback.fire,rejectCallback.fire,notifyCallback.fire);
40 var pom2 = pom1.then(function(param){
41     console.log(param);//oK
42 },function(param){
43     console.log(param);//no
44 },function(param){
45     console.log(param);//why
46 });

關於應用這裏我說三個注意點:第一點是傳入的回調函數列表必定是回調函數的觸發函數fire,而不是回調函數列表Callbacks對象;第二點是若是傳入Callbacks列表對應的then節點的不會執行,可是不影響後面then節點的添加的回調對象的調用,這在Promise異步對象API規範中叫作錯誤吞噬,也就是容錯性,當前回調節點出錯不能影響後面節點的調用執行(後面then節點回調函數仍是使用前面出錯節點傳入的參數做爲執行實參);第三點是當一個then節點的傳入的只是一個自定義的函數,該函數若是有返回值會被傳入一個then節點的回調函數做爲參數(第一個示例能夠印證),若是沒有返回則繼續使用上一個回調節點傳入的參數做爲當前then節點回調函數執行的參數(第二個示例中有印證)。

3.deferred.then源碼部分(此處高能):

先將個人真個deferred方法模擬實現代碼所有貼上,而後再將then實現的幾個關鍵的邏輯提取出來分析:

 1 function Deferred(fuc){
 2     let deferred = {};
 3     let tuples = [
 4         ["resolve","done",Callback("onse memory")],
 5         ["reject","fail",Callback("onse memory")],
 6         ["notify","progress",Callback("memory")]
 7     ];
 8     let promise = {
 9         always:function(fun){ //在jQuery中使用的參數是從實參列表中取得的,是由於源碼中的Callback載添加方法時能處理回調方法列表(數組),在個人模仿代碼中沒有實現這個功能
10             if(fun && typeof fun == "function"){
11                 deferred.done(fun).fail(fun);
12             }
13             return this;
14         },
15         then:function(){
16             let fns = Array.prototype.slice.call(arguments,[0,3]);
17             //爲後續的延遲迴調建立一個新的延遲迴調對象(參數指向新的回調對象)
18             // 詳細參考這段代碼:if(fuc){fuc.call(deferred, deferred);}
19             return Deferred(function(newDefer){
20                 for(let i = 0; i < tuples.length; i++){
21                     let action = tuples[i][0];
22                     let fn;
23                     if(typeof fns[i] == "function"){
24                         fn = fns[i];
25                     }else{
26                         fn = false;
27                     }
28                     deferred[tuples[i][1]](function(){
29                         var returned = fn && fn.apply(this,arguments);
30                         if(returned && typeof returned.promise == "function"){
31                             returned.promise()
32                                 .done(newDefer.resolve)
33                                 .fail(newDefer.reject)
34                                 .progress(newDefer.notify);
35                         }else{
36                             newDefer[action + "With"]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments );
37                         }
38                     });
39                 }
40                 fns = null;
41             }).promise();
42         },
43         promise:function(obj){
44             return obj != null ? clone(promise,obj) : promise;
45         }
46     }
47     let pending = true;//當延遲函數列表被受理或者拒絕之後就就該爲false,表示延遲對象已經調用執行,後面調用受理或者拒絕方法再也不執行
48     for(var tuple in tuples){
49         var list = tuples[tuple][2];
50         promise[tuples[tuple][1]] = list.add;
51         deferred[tuples[tuple][0]] = (function(i,obj){
52             return function(){
53                 if(pending){
54                     deferred[tuples[i][0]+"With"](obj === undefined ? obj : promise, arguments);
55                     tuples[i][0] == "resolve" || tuples[i][0] == "reject" ? pending = false : "";
56                 }
57                 return obj;
58             }
59         })(tuple,this);
60         deferred[tuples[tuple][0] + "With"] = list.fireWith;
61     }
62     promise.promise(deferred);
63     if(fuc){
64         fuc.call(deferred, deferred);
65     }
66     return deferred;
67 }

then的模擬源碼15~42行,先從回調對象執行的總體上來分析他是如何實現鏈式調用:

 

經過將後面鏈式調用的then對應的延遲迴調對象的觸發方法綁定到上一個對應的回調列表中中實現了回調對象各個狀態的鏈式觸發。這個鏈式觸發能夠說是jQuery.Deferred實現最經典,邏輯最複雜的實現,後面的when方法也是基於這個then方法的基礎實現的。

實現這個邏輯採用了一個複雜的做用域鏈切換,下面來看這段基於Deferred.then鏈式調用實現提取的結構模型代碼來理解:

 1 function a(fn){
 2     var text = "text";
 3     var obj = {
 4         fun:function(){
 5             return a(function(){
 6                 console.log(text);
 7             });
 8         }
 9     }
10     if(fn){
11         fn();
12     }
13     return obj;
14 }
15 var obj = a();
16 obj.fun();//最後打印出來的text值來源第一個a執行的做用域

這裏有兩個基礎知識點須要弄明白,第一個就是函數在哪一個做用域聲明,它的執行環境就是那個。第二個就是函數傳參傳的是引用值,因此即使傳入的函數被提高爲當前做用域變量對象上的一個方法,可是實際傳入的這個函數的執行環境仍是攜帶了該函數聲明時的做用域(執行環境)。

上面的第二點是個很是繞的js做用域鏈基礎知識,這種應用在通常的代碼中是不會出現通常的平常開發的代碼中,甚至不少時候這種代碼會被開發規範給摒棄掉,可是爲了實現then這種複雜的功能和邏輯,應用做用域鏈控制能夠減小很是多的代碼以及冗餘,這種實現確定不是惟一方式,可是我只能說這是一個很是巧妙的設計。

而後還有一個關鍵的設計就是面向切面編程,咱們知道then是給當前回調對象添加函數,但同時在添加當前回調函數的時候,咱們還須要將後面的延遲迴調對象的觸發方法添加到當前對象對象上,因此不能直接將then傳入的方法直接添加到當前回調對象上,而是添加一個包裹方法,在這個方法內執行添加的方法,而後還要添加後面一個延遲對象的觸發方法,咱們知道jQurey的延遲對象是基於Callbacks的memory模式實現的,添加後面一個延遲迴調對象的觸發方法就立馬會被觸發,這也就實現了前面視圖的鏈式調用。

關於面向切面編程的思想(示例模型):

 1 var obj = {
 2     foo:function(){
 3         console.log("我是原函數");
 4     }
 5 }
 6 obj.foo = (function(fn){
 7     return function(){
 8         fn();
 9         console.log("這裏要乾點別事");
10     }
11 }(obj.foo));
12 obj.foo();
13 //我是原函數
14 //這裏要乾點別的事

關於then的相關內容本來想分析的更細緻一些,可是當我寫着寫着發現無論怎麼描述其複雜性並不能說就能簡單的幾句話說明白,並且每一個人的基礎都不一樣,都會有不一樣的疑惑,因此我就將實現then的幾個關鍵設計模式在這裏所有提取出來了,若是經過這些提取出來的結構分析還不能理解的話,能夠在評論區給我留言,具體疑問我具體幫助你解答。

相關文章
相關標籤/搜索