jQuery使用():Deferred有状态的回调列表(含源码)
- deferred的功能及其使用
- deferred的实现原理及模拟源码
- deferred.then方法的实现原理及模拟源码
一、deferred的功能及其使用
deferred的底层是基于callbacks实现的,建议再熟悉callbacks的内部机制前提下阅读这篇博客,如果需要了解callbacks可以参考:jQuery使用():Callbacks回调函数列表之异步编程(含源码分析)
- 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)。下面使用这六个方法来实现一个模拟异步状态回调事件:
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方法还有另一个功能就是能连续注册来替代这种缺陷,并且还可以接收来自上一个方法的返回值作为参数:
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方法就是作用在新的异步延迟对象上。
二、deferred的实现原理及模拟源码
这部分源码是基于jQuery使用():Callbacks回调函数列表之异步编程(含源码分析)的模拟回调函数列表对象实现的,没有测试jQuery的Callbacks对象,代码暂时实现了promise()方法,then()方法没有实现,今天有事,有时间再来添加(2019.9.18添加then方法的实现原理分析及模拟源码)。
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 }
三、deferred.then方法实现原理及模拟源码
1.deferred.then方法还是基于promise实现的,然后被克隆到deferred对象上。在jQuery源码中考虑内容篇幅和分析目标是异步回调的实现逻辑,不对promise的具体实现目的分析,这部分会在ES6的promise对象中会具体解析,下面先来看看deferred.then方法具体能实现什么?
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的几个关键设计模式在这里全部提取出来了,如果通过这些提取出来的结构分析还不能理解的话,可以在评论区给我留言,具体疑问我具体帮助你解答。