jQuery源码二周目#8 Deferred

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()了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342