Deferred这个模块能够解决什么?
要在一个异步操作完成时执行某些操作,我们一般是通过回调方法实现的
setTimeout(function() {
// 完成时执行的业务代码...
}, 1000)
如果一个异步方法完成之后去执行另一个异步方法,并且另一个异步方法完成后又执行一个异步方法,这样层层嵌套后代码会是这样的:
setTimeout(function() {
setTimeout(function() {
setTimeout(function() {
// 噼里啪啦一大段的代码...
}, 1000)
}, 1000)
}, 1000)
这种写法有个专门的术语,叫回调地狱。我们希望用一种更加优雅的方式去编写异步操作,比如所像job1.then(job2).then(job3).then(job4)
这样的链式调用实现异步编程。下面介绍一种解决方案
CommonJS Promises/A规范
规范的详细内容可以自行百度,这里介绍该规范的一种实现——Promise。Promise是目前比较流行Javascript异步编程解决方案之一,使用如下
var job1 = new Promise((resolve, reject) => {
setTimeout(function() {
resolve()
}, 1000);
})
job1.then(function() {
console.log('1s后执行');
})
jQuery也给出了解决方案
var def = new $.Deferred();
setTimeout(function() {
def.resolve();
}, 1000)
def.then(function() {
console.log('1s后执行');
})
$.Deferred是怎么实现的
jQuery利用设计模式中的观察者模式去解决异步问题,比起干瘪瘪的学习观察者模式,通过$.Deferred
源码更能明白观察者模式的用途与优秀之处。接下来通过观察者模式去实现异步操作
var callbacks = $.Callbacks();
setTimeout(function() {
callbacks.fire();
}, 1000)
callbacks.add(function() {
console.log('1s后执行');
})
通过上面的代码大概能够明白$.Deferred
的实现思路,接下来要分析源代码了,说实话真的不想分析源码,因为我其实也没看太懂,这一块的源代码我看了特别久,依旧有很多无法理解的地方。我觉得在所有模块中它的难度仅次于Sizzle引擎,算是第二难的模块。分析源码前我先尽可能抛开源码的影响,用自己的理解去实现一个$.Deferred
。
jQuery.extend({
Deferred: function() {
var done = $.Callbacks();
var fail = $.Callbacks();
var deferred = {
done: function (func) {
done.add(func)
},
fail: function (func) {
fail.add(func);
},
resolve: function () {
done.fire()
},
reject: function () {
fail.fire()
}
};
return deferred;
}
})
然后去测试一下
var def = $.Deferred()
setTimeout(function () {
def.resolve();
}, 2000)
def.done(function() {
console.log('2s后执行')
})
2s后控制台准时打印出2s后执行
这句话,意外的很行o( ̄▽ ̄)d
上面的代码其实还有不完善的地方,比如说在触发resolve()方法后就不再触发reject()方法。接下来仿照着源码去完善这一功能
jQuery.extend({
Deferred: function() {
var
// 把有共同特性的代码合并成一种结构
// once模式:resolve和reject只要触发过一次,后面就不能再被触发了(这个好像是CommonJS Promises/A中规定的)
// memory模式:如果异步方法执行太快,在还没有添加后续操作就已经完成了,这样后续操作就不会执行。为了避免这种情况所以使用memory模式
tuples = [
["notify", "progress", jQuery.Callbacks("memory")],
["resolve", "done", jQuery.Callbacks("once memory"), "resolved"],
["reject", "fail", jQuery.Callbacks("once memory"), "rejected"]
],
// 初始状态为pending,最终状态为resolved或者rejected,一旦状态变更为resolved和rejected中的一种后,状态将不可变更
// 这是在CommonJS Promises/A中明确规定的
state = "pending",
deferred = {};
jQuery.each(tuples, function (item, i) {
var list = this[2];
deferred[this[0]] = list.fire; // notify resolve reject
deferred[this[1]] = list.add; // progress done fail
var stateString = this[3]; // pending resolved rejected
// 当触发了resolve()方法后就不再出发reject()方法,同理触发了reject()后不再触发resolve()
// 这也是在CommonJS Promises/A中明确规定的
if (stateString) {
// 所以接下来的步骤就是
// 变更状态
// 禁用reject()方法(以触发resolve()方法的情况为例)
// 给notify()方法上锁
list.add(
function () {
state = stateString;
},
tuples[3 - i][2].disable,
tuples[0][2].lock
);
}
})
return deferred;
}
})
主要结构就是这样,如果只是应付简单的业务场景,上面的代码已经足够了。但是在jQuery源码中,还多了个promise
对象,这个对象中有一个很常用的方法then()
。接下来去一步一步的实现这个then()
方法,至于其他方法就不管了,不常用的方法实现了也没意义。
then()方法
实现then()
方法之前先对上面的代码做一个改动
jQuery.extend({
Deferred: function(func) {
var
// 把有共同特性的代码合并成一种结构
// once模式:resolve和reject只要触发过一次,后面就不能再被触发了(这个好像是CommonJS Promises/A中规定的)
// memory模式:如果异步方法执行太快,在还没有添加后续操作就已经完成了,这样后续操作就不会执行。为了避免这种情况所以使用memory模式
tuples = [
["resolve", "done", jQuery.Callbacks("once memory"), "resolved"],
["reject", "fail", jQuery.Callbacks("once memory"), "rejected"],
["notify", "progress", jQuery.Callbacks("memory")]
],
// 初始状态为pending,最终状态为resolved或者rejected,一旦状态变更为resolved和rejected中的一种后,状态将不可变更
// 这是在CommonJS Promises/A中明确规定的
state = "pending",
promise = {
promise: function(obj) {
return obj != null ? jQuery.extend(obj, promise) : promise;
}
},
deferred = {};
jQuery.each(tuples, function (tuple, i) {
var list = tuple[2], // Callbacks对象
stateString = tuple[3]; // 状态(pending、resolved、rejected中的一种)
// promise.progress = list.add
// promise.done = list.add
// promise.fail = list.add
promise[tuple[1]] = list.add;
// 当触发了resolve()方法后就不再出发reject()方法,同理触发了reject()后不再触发resolve()
// 这也是在CommonJS Promises/A中明确规定的
if (stateString) {
// 所以接下来的步骤就是
// 变更状态
// 禁用reject()方法(以触发resolve()方法的情况为例)
// 给notify()方法上锁
list.add(
function () {
state = stateString;
},
tuples[i ^ 1][2].disable,
tuples[0][2].lock
);
}
// deferred.notify = function() { deferred.notifyWith(...) }
// deferred.resolve = function() { deferred.resolveWith(...) }
// deferred.reject = function() { deferred.rejectWith(...) }
deferred[tuple[0]] = function() {
deferred[tuple[0] + 'With']( this === deferred ? undefined : this, arguments );
return this;
};
// deferred.notifyWith = list.fireWith
// deferred.resolveWith = list.fireWith
// deferred.rejectWith = list.fireWith
deferred[tuple[0] + 'With'] = list.fireWith;
})
promise.promise(deferred);
// 回调函数
// 通过回调获取到$.Deferred()方法创建的deferred对象
if(func) {
func.call(deferred, deferred);
}
return deferred;
}
})
改动的部分都是照搬的jQuery源码,我也不知道为什么要这样写,我要是知道了就不会照搬了。我看了下then()
方法的源码,还是比较的复杂,一大段代码一时半会还真不知道什么意思。我干脆先实现then()
方法的基本功能,传入两个方法作为参数,第一个方法是在resolve时调用,第二个方法在reject时调用。
then: function () {
var fns = arguments;
jQuery.each(tuples, function(tuple, i) {
deferred[tuple[1]](fns[i]);
})
}
上面的then()
方法还不完整,jQuery的then()
方法是能够job1.then(job2).then(job3).then(job4)
这样调用的。我看了眼最新版jquery-3.5.1.js的then()
方法源码,特别长的一段,我是不想去琢磨这么长一串代码什么意思。所以我找了个早期版本的then()
方法源码,长度刚好合适
then: function () {
var fns = arguments;
return jQuery.Deferred(function(newDefer) {
jQuery.each(tuples, function(tuple, i) {
var action = tuple[0],
fn = isFunction(fns[i]) && fns[i];
deferred[tuple[1]] (function() {
var returned = fn && fn.apply(this, arguments);
if (returned && isFunction(returned.promise)) {
returned.promise()
.done(newDefer.resolve())
.fail(newDefer.reject)
.progress(newDefer.notify);
} else {
newDefer[action + 'With'](this === promise ? newDefer.promise() : this, fn ? [returned] : arguments)
}
})
})
})
},
then()方法做了几件事情:
1.创建一个Deferred对象,名称为newDefer
。
2.分别调用deferred.done()
、deferred.fail()
、deferred.progress()
方法传入成功时执行的方法、失败时执行的方法、progress方法。
3.再次调用deferred.done(newDefer.resolveWith)
、deferred.fail(newDefer.rejectWith)
、deferred.progress(newDefer.notifyWith)
。
4.返回newDefer
对象。
然后把上面的源码再做一个简化就是
then: function () {
var fns = arguments;
return jQuery.Deferred(function(newDefer) {
jQuery.each(tuples, function(tuple, i) {
var action = tuple[0];
deferred[tuple[1]](fns[i]);
deferred[tuple[1]](function () {
newDefer[action + 'With'](this, arguments);
});
})
})
}
这里有两个可能会让人疑惑的地方
1.jQuery.Deferred(func)
和jQuery.Deferred()
到底有什么不同
jQuery.Deferred(function(newDefer) {
// ...
})
可以看见then()源码第二行就是如下结构,它不是单纯的调用jQuery.Deferred()
而是传入了一个方法作为参数,而Deferred
源码中是如下处理的传入方法的
if(func) {
func.call(deferred, deferred);
}
其实这就是一个简单的回调函数,调用jQuery.Deferred()
创建一个deferred
对象,通过回调函数可以操作创建的deferred
对象,就这么简单。
2.newDefer[action + 'With'](this, arguments);
中的arguments
指向谁
deferred[tuple[1]](function () {
newDefer[action + 'With'](this, arguments);
});
指向调用deferred.resolve()
时传入的参数,这么说可能太模糊了,只能自行体会。
when()方法
用于合并多个异步操作,统一处理
$.when(d1,d2,d3......).done(function(v1, v2,v3...) {
// 等待所有异步加载完毕后执行
});
然后自己去实现一个$.when()
方法
when: function () {
var length = arguments.length;
var master = jQuery.Deferred();
jQuery.each(arguments, function(deferred) {
deferred.done(function () {
if (--length === 0) {
master.resolve();
}
});
})
return master;
}
可以看出上面的代码非常的粗糙,但是基本的实现思想已经阐述清楚了,接下来只需要添加亿点点细节,它就能变成jQuery源码的样子。在完善代码之前, 我完整的看了一遍源码,然后蒙上源码,像做闭卷考试一样,仿照源码写了一个自己的代码
when: function () {
var
remaining = arguments.length, // 剩余执行数,原始值=传入Deferred对象个数,每执行一个Deferred该值就-1
fns = arguments, // Deferred对象列表
resolveContexts = [], // 上下文环境,供master.resolveWith()方法使用
resolveValues = [], // 用于收集每个Deferred对象调用resolve()方法时传入的参数,供master.resolveWith()方法使用
master = jQuery.Deferred(); // when()方法返回的对象,用于执行后续操作
// 简单说说这个方法的作用
// when()方法的作用是当所有的Deferred对象执行完成,才去执行某一个方法。那么如何保证所有的Deferred对象执行完成?
// 用一个计数器(remaining),每执行完一个Deferred,计数器就-1,等计数器减到0的时候去触发master.resolveWith()
// 另外这个方法还做了两件事就是收集上下文(resolveContexts)和传入参数(resolveValues),供master.resolveWith()调用时使用
// 我个人觉得上下文(resolveContexts)的收集倒不是很重要,随便传入个this就行了,只要把每个Deferred的传入参数(resolveValues)收集了就足够了
function updateFunc(i) {
return function() {
resolveContexts[i] = this;
resolveValues[i] = arguments.length > 1 ? slice.call(arguments) : arguments[0];
if (--remaining <= 0) {
master.resolveWith(resolveContexts, resolveValues);
}
}
}
for(var i = 0; i < remaining; i++) {
if (isFunction(fns[i].promise)) { // 判断是否是Deferred对象
fns[i].done(updateFunc(i)).fail(master.reject);
}
}
return master;
}
需要注意的一点是运行时fns[i].done(updateFunc(i)).fail(master.reject);
这行代码会报错,这是因为之前我写的Callbacks.add()方法是没有返回值的,我当时觉得源码中的返回值没什么用就给干掉了,没想到居然会在这里显现作用,这就说明存在必定有它的意义。
上面的代码也没什么好说的,唯一可能难以理解的就是updateFunc()
方法,只要把updateFunc()
方法做的事看懂了,那基本上算是弄懂了$.when()
了。