• 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)。下面使用这六个方法来实现一个模拟异步状态回调事件:

 var df = $.Deferred();
//注册成功的回调函数
df.done(function(a){
console.log('ho yeah I do it!' + a);
});
//注册失败的回调函数
df.fail(function(a){
console.log('sorry I am loser...' + a);
});
//注册进行时的函数
df.progress(function(a){
console.log("waiting???" + a);
});
//用定时器模拟一个异步状态回调事件 >60 表示成功 ; < 50表示失败 ; 60><50表示正在进行
setInterval(function(){
var score = Math.random() * 100;
if(score > 60){
df.resolve("simpleness");
}else if(score < 50){
df.reject("difficult");
}else{
df.notify('be surprised to be dumb');
}
},1000);

通过deferred对象同时管理三个回调对象,让代码语义化实现的更优雅,同时也降低了代码的冗余。添加回调函数的形式于callback。add方法一致,可以实现多个同时添加。但是deferred做的远远不止这些,由于deferred对象是同时存在添加和执行两种方法,为了保证调用只能在特定位置触发,deferred还实现了promise对象只用来实现注册方法,promis对象上只有指向回调对象add的done、fail、progress方法,以及一个then用来更便捷的添加回调函数的方法。

所以上面的方法可以修改为:

 //用定时器模拟一个异步状态回调事件 >60 表示成功 ; < 50表示失败 ; 60><50表示正在进行
function createScore(){
var df = $.Deferred();
setInterval(function(){
var score = Math.random() * 100;
if(score > 60){
df.resolve("simpleness");
}else if(score < 50){
df.reject("difficult");
}else{
df.notify('be surprised to be dumb');
}
},1000);
return df.promise();
}
var pom = createScore();
//注册成功的回调函数
pom.done(function(a){
console.log('ho yeah I do it!' + a);
});
//注册失败的回调函数
pom.fail(function(a){
console.log('sorry I am loser...' + a);
});
//注册进行时的函数
pom.progress(function(a){
console.log("waiting???" + a);
});

上面的代码修改后,就是一个盗版的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方法还有另一个功能就是能连续注册来替代这种缺陷,并且还可以接收来自上一个方法的返回值作为参数:

 pom.then(function(a){
console.log('ho yeah I do it!' + a);
return "oK"
},function(a){
console.log('sorry I am loser...' + a);
return "no"
},function(a){
console.log("waiting???" + a);
return "why"
}).then(function(param){
console.log(param);//oK
},function(param){
console.log(param);//no
},function(param){
console.log(param);//why
});

但是需要注意的是,返回值不能是新的deferred对象,如果是一个新的异步延迟对象返回,后面继续使用then方法就是作用在新的异步延迟对象上。

二、deferred的实现原理及模拟源码

这部分源码是基于jQuery使用():Callbacks回调函数列表之异步编程(含源码分析)的模拟回调函数列表对象实现的,没有测试jQuery的Callbacks对象,代码暂时实现了promise()方法,then()方法没有实现,今天有事,有时间再来添加(2019.9.18添加then方法的实现原理分析及模拟源码)。

 function clone(origin, target){
for(var ele in origin){
target[ele] = origin[ele];
}
return target;
}
function Deferred(fuc){
//异步延迟对象
var deferred = {}
var tuples = [
["resolve","done",Callback("once memory")],
["reject","fail",Callback("once memory")],
["notify","progress",Callback("memory")]
];
var statesum = true;
//异步延迟对象的注册对象
var promise = {
//返回deferred的promise注册对象
//源码中有obj的合并采用extend实现,这里写了一个简单的克隆方法
promise:function(obj){
return obj != null ? clone(promise,obj) : promise;
}
}
var pending = true;
for(var tuple in tuples){
var list = tuples[tuple][2];
//添加deferred的回调函数注册方法 done fail progress
promise[tuples[tuple][1]] = list.add;
//添加deferred的回调函数触发执行方法 resolve reject ontify
deferred[tuples[tuple][0]] = (function(i,obj){
return function(){
if(pending){
// console.log(this.state);
deferred[tuples[i][0]+"With"](obj === deferred ? obj : promise,arguments);
tuples[i][0] == "resolve" || tuples[i][0] == "reject" ? pending = false : "";
}
return obj;
}
})(tuple,this);
deferred[tuples[tuple][0] + "With"] = list.fireWith;
}
//将promise合并到deferred上 -- 同样采用克隆方法实现
promise.promise(deferred);
//fcn执行 上下文指向deferred 参数设置为deferred
if(fuc){
fuc.call(deferred,deferred);
}
return deferred;
}

三、deferred.then方法实现原理及模拟源码

1.deferred.then方法还是基于promise实现的,然后被克隆到deferred对象上。在jQuery源码中考虑内容篇幅和分析目标是异步回调的实现逻辑,不对promise的具体实现目的分析,这部分会在ES6的promise对象中会具体解析,下面先来看看deferred.then方法具体能实现什么?

2.deferred.then方法的语法:

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

以上两种参数的传入其实是一种传参,只是第二个方式告诉我们可以传入回调函数列表的触发函数,本质传入的三个参数都要是函数类型。先来看一下下面这个示例代码:

 // deferred测试采用jQuery源码测试
function ddd(){
var df = $.Deferred();
setInterval(function(){
var score = Math.random() * 100;
if(score > 60){
df.resolve("simpleness");
}else if(score < 50){
df.reject("difficult");
}else{
df.notify('be surprised to be dumb');
}
},1000);
return df.promise();
}
var pom = ddd();
pom.then(function(a){
console.log('ho yeah I do it!' + a);
return "oK"
},function(a){
console.log('sorry I am loser...' + a);
return "no"
},function(a){
console.log("waiting???" + a);
return "why"
}).then(function(param){
console.log(param);//oK
},function(param){
console.log(param);//no
},function(param){
console.log(param);//why
});

前面说过也可以传入回调函数列表的触发方法,用下面这个改造的示例代码来测试:

 // deferred测试
function ddd(){
var df = $.Deferred();
setInterval(function(){
var score = Math.random() * 100;
if(score > 60){
df.resolve("simpleness");
}else if(score < 50){
df.reject("difficult");
}else{
df.notify('be surprised to be dumb');
}
},1000);
return df.promise();
}
var pom = ddd();
var resolveCallback = $.Callbacks();
var rejectCallback = $.Callbacks();
var notifyCallback = $.Callbacks(); resolveCallback.add(function(a){
console.log(a + ":1");
},function(a){
console.log(a + ":2");
},function(a){
console.log(a + ":3");
});
rejectCallback.add(function(a){
console.log(a + ":1");
},function(a){
console.log(a + ":2");
},function(a){
console.log(a + ":3");
});
notifyCallback.add(function(a){
console.log(a + "继续");
}); var pom1 = pom.then(resolveCallback.fire,rejectCallback.fire,notifyCallback.fire);
var pom2 = pom1.then(function(param){
console.log(param);//oK
},function(param){
console.log(param);//no
},function(param){
console.log(param);//why
});

关于应用这里我说三个注意点:第一点是传入的回调函数列表一定是回调函数的触发函数fire,而不是回调函数列表Callbacks对象;第二点是如果传入Callbacks列表对应的then节点的不会执行,但是不影响后面then节点的添加的回调对象的调用,这在Promise异步对象API规范中叫做错误吞噬,也就是容错性,当前回调节点出错不能影响后面节点的调用执行(后面then节点回调函数还是使用前面出错节点传入的参数作为执行实参);第三点是当一个then节点的传入的只是一个自定义的函数,该函数如果有返回值会被传入一个then节点的回调函数作为参数(第一个示例可以印证),如果没有返回则继续使用上一个回调节点传入的参数作为当前then节点回调函数执行的参数(第二个示例中有印证)。

3.deferred.then源码部分(此处高能):

先将我的真个deferred方法模拟实现代码全部贴上,然后再将then实现的几个关键的逻辑提取出来分析:

 function Deferred(fuc){
let deferred = {};
let tuples = [
["resolve","done",Callback("onse memory")],
["reject","fail",Callback("onse memory")],
["notify","progress",Callback("memory")]
];
let promise = {
always:function(fun){ //在jQuery中使用的参数是从实参列表中取得的,是因为源码中的Callback载添加方法时能处理回调方法列表(数组),在我的模仿代码中没有实现这个功能
if(fun && typeof fun == "function"){
deferred.done(fun).fail(fun);
}
return this;
},
then:function(){
let fns = Array.prototype.slice.call(arguments,[0,3]);
//为后续的延迟回调创建一个新的延迟回调对象(参数指向新的回调对象)
// 详细参考这段代码:if(fuc){fuc.call(deferred, deferred);}
return Deferred(function(newDefer){
for(let i = 0; i < tuples.length; i++){
let action = tuples[i][0];
let fn;
if(typeof fns[i] == "function"){
fn = fns[i];
}else{
fn = false;
}
deferred[tuples[i][1]](function(){
var returned = fn && fn.apply(this,arguments);
if(returned && typeof returned.promise == "function"){
returned.promise()
.done(newDefer.resolve)
.fail(newDefer.reject)
.progress(newDefer.notify);
}else{
newDefer[action + "With"]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments );
}
});
}
fns = null;
}).promise();
},
promise:function(obj){
return obj != null ? clone(promise,obj) : promise;
}
}
let pending = true;//当延迟函数列表被受理或者拒绝以后就就该为false,表示延迟对象已经调用执行,后面调用受理或者拒绝方法不再执行
for(var tuple in tuples){
var list = tuples[tuple][2];
promise[tuples[tuple][1]] = list.add;
deferred[tuples[tuple][0]] = (function(i,obj){
return function(){
if(pending){
deferred[tuples[i][0]+"With"](obj === undefined ? obj : promise, arguments);
tuples[i][0] == "resolve" || tuples[i][0] == "reject" ? pending = false : "";
}
return obj;
}
})(tuple,this);
deferred[tuples[tuple][0] + "With"] = list.fireWith;
}
promise.promise(deferred);
if(fuc){
fuc.call(deferred, deferred);
}
return deferred;
}

then的模拟源码15~42行,先从回调对象执行的整体上来分析他是如何实现链式调用:

通过将后面链式调用的then对应的延迟回调对象的触发方法绑定到上一个对应的回调列表中中实现了回调对象各个状态的链式触发。这个链式触发可以说是jQuery.Deferred实现最经典,逻辑最复杂的实现,后面的when方法也是基于这个then方法的基础实现的。

实现这个逻辑采用了一个复杂的作用域链切换,下面来看这段基于Deferred.then链式调用实现提取的结构模型代码来理解:

 function a(fn){
var text = "text";
var obj = {
fun:function(){
return a(function(){
console.log(text);
});
}
}
if(fn){
fn();
}
return obj;
}
var obj = a();
obj.fun();//最后打印出来的text值来源第一个a执行的作用域

这里有两个基础知识点需要弄明白,第一个就是函数在哪个作用域声明,它的执行环境就是那个。第二个就是函数传参传的是引用值,所以即便传入的函数被提升为当前作用域变量对象上的一个方法,但是实际传入的这个函数的执行环境还是携带了该函数声明时的作用域(执行环境)。

上面的第二点是个非常绕的js作用域链基础知识,这种应用在一般的代码中是不会出现一般的日常开发的代码中,甚至很多时候这种代码会被开发规范给摒弃掉,但是为了实现then这种复杂的功能和逻辑,应用作用域链控制可以减少非常多的代码以及冗余,这种实现肯定不是唯一方式,但是我只能说这是一个非常巧妙的设计。

然后还有一个关键的设计就是面向切面编程,我们知道then是给当前回调对象添加函数,但同时在添加当前回调函数的时候,我们还需要将后面的延迟回调对象的触发方法添加到当前对象对象上,所以不能直接将then传入的方法直接添加到当前回调对象上,而是添加一个包裹方法,在这个方法内执行添加的方法,然后还要添加后面一个延迟对象的触发方法,我们知道jQurey的延迟对象是基于Callbacks的memory模式实现的,添加后面一个延迟回调对象的触发方法就立马会被触发,这也就实现了前面视图的链式调用。

关于面向切面编程的思想(示例模型):

 var obj = {
foo:function(){
console.log("我是原函数");
}
}
obj.foo = (function(fn){
return function(){
fn();
console.log("这里要干点别事");
}
}(obj.foo));
obj.foo();
//我是原函数
//这里要干点别的事

关于then的相关内容原本想分析的更细致一些,但是当我写着写着发现不管怎么描述其复杂性并不能说就能简单的几句话说明白,而且每个人的基础都不同,都会有不同的疑惑,所以我就将实现then的几个关键设计模式在这里全部提取出来了,如果通过这些提取出来的结构分析还不能理解的话,可以在评论区给我留言,具体疑问我具体帮助你解答。

jQuery使用():Deferred有状态的回调列表(含源码)的更多相关文章

  1. jQuery使用():Callbacks回调函数列表之异步编程(含源码分析)

    Callbacks的基本功能回调函数缓存与调用 特定需求模式的Callbacks Callbacks的模拟源码 一.Callbacks的基本功能回调函数缓存与调用 Callbacks即回调函数集合,在 ...

  2. 【.net+jquery】绘制自定义表单(含源码)

    前言 两年前在力控的时候就想做一个类似的功能,当时思路大家都讨论好了,诸多原因最终还是夭折了.没想到两年多后再这有重新提出要写一个绘制表单的功能.对此也是有点小激动呢?总共用时8.5天的时间基本功能也 ...

  3. jQuery LigerUI V1.2.2 (包括API和全部源码) 发布

    前言 这次版本主要对树进行了加载性能上面的优化,并解决了部分兼容性的问题,添加了几个功能点. 欢迎使用反馈. 相关链接 API:         http://api.ligerui.com/ 演示地 ...

  4. 基于JQuery EasyUI的WebMVC控件封装(含源码)

    JQuery EasyUI类库,大家不会陌生,出来已经有很多年了.个人感觉还是很好用的,作者更新频率也很快,bug也及时修复. 最近在整理以前的代码,找到了这个组件,它是将EasyUI组件封装成MVC ...

  5. HTML5+CSS3+Jquery实现纯手工的垂直时光轴【附源码】

    前言 由于工作中需要,系统中需要记录不同时间发生的事件,为了提升用户体验,决定用时光轴来实现.[据说这个东西挺火的,QQ空间和FB都在用...] 这个时光轴是在 三生石上 这位博主的时光轴基础上修改的 ...

  6. Java+JQuery实现网页显示本地文件目录(含源码)

    原文地址:http://www.cnblogs.com/liaoyu/p/uudisk.html 源码地址:https://github.com/liaoyu/uudisk 前段时间为是练习JQuer ...

  7. jQuery实现多级手风琴树形下拉菜单(源码)

    前几天因为公司的菜单要调整,公司的UI框架是不支持的,所以就自己在网上找了一个下拉菜单,可以支持多级菜单数据的,菜单数据是从xml文件中配置后读取的,网上有许多这方面的例子感觉不是很好用,就打了个包贴 ...

  8. MVVM架构~knockoutjs系列之表单添加(验证)与列表操作源码开放

    返回目录 本文章应该是knockoutjs系列的最后一篇了,前几篇中主要讲一些基础知识,这一讲主要是一个实际的例子,对于一个对象的添加与编辑功能的实现,并将项目源代码公开了,共大家一起学习! knoc ...

  9. shiro实现无状态的会话,带源码分析

    转载请在页首明显处注明作者与出处 朱小杰      http://www.cnblogs.com/zhuxiaojie/p/7809767.html 一:说明 在网上都找不到相关的信息,还是翻了大半天 ...

随机推荐

  1. UE4游戏开发基础命令

    在个人的Unrealengine账户中关联自己的GitHub账户成功之后,就可以访问UE4引擎的源码了. git clone -b release https://github.com/EpicGam ...

  2. 阿里云服务器部署Java Web项目全过程

    最近需要将一个Java Web项目部署到服务器上,方便多人共享访问.这也是我第一次接触服务器之类的东西,也花了一点时间,最终总算部署成功,写下一篇文章记录以便日后回顾. 购买服务器 第一步当然是需要购 ...

  3. Navicat for MySQL破解版安装

    https://pan.baidu.com/s/1OfFPvqrTqbUAC_Eqq2i0KA 提取码:jgep 点击第一个应用程序一路安装即可. 安装成功之后,再点击第二个应用程序PatchNavi ...

  4. SQL 知道字段名 全表搜索此字段属于哪个表

    SELECT name FROM sysobjects WHERE id IN (SELECT ID FROM syscolumns WHERE name='字段名')

  5. 三分钟明白 Activiti工作流 -- java运用

    原文地址:https://blog.csdn.net/jiangyu1013/article/details/73250902 一. 什么是工作流 以请假为例,现在大多数公司的请假流程是这样的 员工打 ...

  6. 修改DEDECMS文章标题长度,解决DEDECMS文章标题显示不全

    dedecms系统使用过程中,常遇到输入的标题原本输入的字数跟保存成功后的数字长度不一样,这个是因为 织梦dedecms系统默认的文章标题字数限制是60,也就是只能输入30个汉字,超过的会自动截断删除 ...

  7. NodeJs之定时器与队列

    NodeJs之定时器与队列 一,介绍与需求 1.1,介绍 定时任务(node-schedule),是针对Node.js的一种灵活的cron-like和not-cron-like作业调度程序.它允许您使 ...

  8. elk部署之前注意事项

    注意事项: 1.不能使用root用户登录,需要是用root 之外的用户登录到系统. 2.centos系统 运行内存不能小于2G,若低于2G需要修改jvm. vi  {jvm_home}/config/ ...

  9. ES6 Promise 用法讲解

    Promise是一个构造函数,自己身上有all.reject.resolve这几个眼熟的方法,原型上有then.catch等同样很眼熟的方法. 那就new一个 var p = new Promise( ...

  10. NIO的初步入门

    NIO java NIO简介 Java NIO 简介 是从java1.4版本开始引入的一个新的IO AP可以替代标准java  IO API NIO与原来的IO有同样的作用和目的,但是使用方式完全不同 ...