背景
- 最近研究jquery发现jq很早就有类似promise的功能了,我jq用的比较少,主要拿来学习设计思想。
- 这个jq的$ajax自1.5以后会返回延迟对象,可以使用.done之类的延迟对象使用的方法来进行后续操作。
- 主要还是看
$.deferred
这个函数,这个函数其实是基于$.callback
这个来实现的。 - 我们把deferred进行简化一下,看这玩意跟promise到底有啥区别。
使用
var wait = function() {
var der = $.Deferred(); //延迟对象 deferred
console.log("函数开始运行");
var test = function() {
der.resolve("运行完毕需要处理"); //成功 队列中的处理函数 调用成功这个队列中的处理函数
}
setTimeout(test, 2000);
return der; //延迟对象
}
//延迟对象的状态 决定调用那个队列中的处理函数
$.when(wait()) //promise对象promise.done() self.add
.done(function(res) {
console.log(res+'3324234')
console.log("执行成功");
})
.fail(function() {
console.log("执行失败");
});
- jquery做的这个延迟对象,从使用上看感觉像是包了一层function的promise。
- 就有点像:
let p1 = function(){
return new Promise((resolve,reject)=>{
console.log('函数开始运行');
setTimeout(() => {
resolve('运行完毕需要处理')
}, 1000);
})
}
p1().then((res)=>{
console.log(res+'213123');
console.log('执行成功');
})
实现
- promise实现就不写了,上次写过了【javascript】手写Promise分步骤超详尽解答(超越PromiseA+标准)
。 - deferred实现是借助于callback函数,callback实际就是个数组,调用add可以往数组里添加函数方法,调用fire可以执行数组里函数,这个里面有点复杂的就是callback允许传参数:
- once: 确保这个回调列表只执行一次(像一个递延 Deferred).
memory: 保持以前的值和将添加到这个列表的后面的最新的值立即执行调用任何回调 (像一个递延 Deferred).
unique: 确保一次只能添加一个回调(所以有没有在列表中的重复).
stopOnFalse: 当一个回调返回false 时中断调用 - callback的设计思想如果不算传参的话很简单,就是一个数组存着一堆func,等用fire的时候foreach执行即可。
- 先看代码:
callbacks: function(options) {
options = typeof options === "string" ? (optionsCache[options] || createOptions(options)) : {};
var list = [];
var index, length, testting, memory, start, starts;
var fire = function(data) {
memory = options.memory && data;
index = starts || 0;
start = 0;
testting = true;
length = list.length;
for (; index < length; index++) {
if (list[index].apply(data[0], data[1]) === false && options.stopOnfalse) {
break;
}
}
}
var self = {
add: function() {
var args = Array.prototype.slice.call(arguments);
start = list.length;
args.forEach(function(fn) {
if (toString.call(fn) === "[object Function]") {
list.push(fn);
}
});
if (memory) {
starts = start;
fire(memory);
}
return this;
},
//指定上下文对象
fireWith: function(context, arguments) {
var args = [context, arguments];
if (!options.once || !testting) {
fire(args);
}
},
//参数传递
fire: function() {
self.fireWith(this, arguments);
}
}
return self;
},
function createOptions(options) {
var object = optionsCache[options] = {};
options.split(/\s+/).forEach(function(value) {
object[value] = true;
});
return object;
}
-
这里面有个设计思路跟node的流很像,真正的执行函数在别的地方,在self里放入的fire用来传参和this,传给firewith,firewith在传给真正的fire函数。其实这么写说实话感觉有点兜了一圈。
-
真正的fire为什么用for循环写?主要是为了配置参数stopOnFalse,有这个参数的话,如果返回值为false就break掉,后面不会执行。
-
说一下callback这个传入参数,会做成个对象,比如once memory,就靠空格做成option对象,后面通过对象获取参数。
-
说一下once这个参数,就是只执行一次,是通过testing这个参数来进行判断,如果走了次真正的fire函数,那么就给testing加true,在第二次用户调用fire的时候,会执行firewith,进行判断,如果没有配置once或者testing没有true就可以再次执行,否则就不能执行了。
-
说一下memory这个参数,就是在第一次执行后进入真正的fire里用个memory变量把参数给记着,第二次添加函数的时候,先看列表里有几个函数,然后把参数传进真正的fire,真正的fire用设置的起始位置开始执行函数,把记着的参数甩给他。这样就能确保不会把上一次add的函数执行。
-
总结一下callback技巧,总体来说没啥东西,设置参数主要还是靠弄个变量然后进行判断。拿到的callback返回值从本质上来说就是个闭包,就是数组在这个闭包里面。而外部是不能通过callback的返回值点list来获取数组的,而返回值是self对象,这个对象又去引用了本应该销毁的callbacks数组。从这里可以发现,我们手写的promise实际是new一个实例做的,会暴露出函数数组,而这里callback做成了闭包,不会暴露数组。当然系统原生的promise不会暴露数组。
-
下面看deferred函数。
Deferred: function(func) {
var tuples = [
["resolve", "done", jQuery.callbacks("once memory"), "resolved"],
["reject", "fail", jQuery.callbacks("once memory"), "rejected"],
["notify", "progress", jQuery.callbacks("memory")]
],
state = "pending",
promise = {
state: function() {
return state;
},
promise: function(obj) {
return obj != null ? jQuery.extend(obj, promise) : promise;
}
},
deferred = {};
tuples.forEach(function(tuple, i) {
var list = tuple[2],
stateString = tuple[3];
// promise[ done | fail | progress ] = list.add
promise[tuple[1]] = list.add;
// Handle state
if (stateString) {
list.add(function() {
// state = [ resolved | rejected ]
state = stateString;
});
}
// deferred[ resolve | reject | notify ]
deferred[tuple[0]] = function() {
deferred[tuple[0] + "With"](this === deferred ? promise : this, arguments);
return this;
};
deferred[tuple[0] + "With"] = list.fireWith;
});
// Make the deferred a promise
promise.promise(deferred);
return deferred;
},
//执行一个或多个对象的延迟对象的回调函数
when: function(subordinate) {
return subordinate.promise();
},
});
- 他的思路是这样:先做个3个数组流程,每个数组中一个元素用callback生成数组。然后用foreach把一个空对象上根据数组加上各种方法。其中promise对象上的done|fail|progress等于callback的add方法,而这个promise对象最终会通过里面的promise方法扩展到deferred上。然后这个resolvewith方法就是callback的fire方法,也添加到deferred上。这样拿到的deferred对象上就有3种状态对应的添加和执行方法。然后通过when来调用promise方法,返回内部的promise对象,这个对象上的方法在deferred上都有,但是权限比deferred少了些,拿不到执行方法,从而不能事先执行导致改变其状态。用户函数看tuples里每个数组第一个就是resolve|reject|notify挂载到延迟对象上,而这个东西就是回调参数,也就是callback的fire方法,由于此时数组同步模式下是空,所以执行了相当于传参,而异步模式下,这个数组里是有东西的,就能直接执行,用户在用done|fail|progress时候会去执行callback的add,由于callback是memory模式,所以会去找到前面resolve|reject|notify的参数再去执行新添加的add。如果异步模式,那参数就还没取到,直接添加进数组等待resolve回调完成即可。
- 这个deferred实际还有很多缺点,比如throw个error就不会走fail(原版也是这样)。另外这个没加上状态判断,原版是通过callback里的lock和disable来清空数组从而达到状态变化后就不会执行的,这个绕的有点晕就不写了。
- 总结下deferred的技巧,同步模式下,数组里一开始没有函数而只有参数,真正的执行通过add来执行。而异步模式下,数组里存放了函数,执行就由fire来执行。这个方式就跟promise有很大区别,promise是通过3种状态来进行操作(同步状态走resolve reject,异步状态走pending),这个callback的逻辑跟Promise相比更难懂一点(主要是太绕,太能复用)。另外就是返回的问题,deffered返回是受控的延迟对象(deferred经promise执行后的对象),而promise返回的就是个新的promise实例。从设计模式上说,deferred相当于你调用它,它甩个闭包数组给你操控,promise是new它产生实例来操作。一个是工厂模式,一个是构造函数模式,很多教科书上说什么工厂模式没有解决对象识别的问题,其实就是个原型链问题,感觉就是鸡蛋里挑骨头。这里的用构造函数模式还是工厂模式很难说谁好谁不好。