美文网首页jQuery源码笔记.jpg
jQuery源码二周目#8 Deferred

jQuery源码二周目#8 Deferred

作者: 柠檬果然酸 | 来源:发表于2020-10-13 18:25 被阅读0次

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

    相关文章

      网友评论

        本文标题:jQuery源码二周目#8 Deferred

        本文链接:https://www.haomeiwen.com/subject/qlakpktx.html