软件世界网 购物 网址 三丰软件 | 小说 美女秀 图库大全 游戏 笑话 | 下载 开发知识库 新闻 开发 图片素材
多播视频美女直播
↓电视,电影,美女直播,迅雷资源↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
移动开发 架构设计 编程语言 Web前端 互联网
开发杂谈 系统运维 研发管理 数据库 云计算 Android开发资料
  软件世界网 -> Web前端 -> jQuery源码解析(2)——Callback、Deferred异步编程 -> 正文阅读

[Web前端]jQuery源码解析(2)——Callback、Deferred异步编程


闲话


这篇文章,一个月前就该出炉了。跳票的原因,是因为好奇标准的promise/A+规范,于是学习了es6的promise,由于兴趣,又完整的学习了《ECMAScript 6入门》
本文目的在于解析jQuery对的promise实现(即Deferred,是一种非标准的promise实现),顺便剖析、挖掘观察者模式的能力。建议读完后参考下面这篇博文的异步编程部分,了解Promise、Generator、Async。
ECMAScript 6规范总结(长文慎入) http://blog.csdn.net/vbdfforever/article/details/50727462

引子


传统的异步编程使用回调函数的形式,当回调函数中调用回调函数时,层层嵌套,且每个回调内部都需要单独捕捉错误,因为执行上下文在同步执行的过程中早就消失无影,无法追溯了。
/* 回调函数 */
step1(function (error, value1) {
    step2(value1, function(error, value2) {
        try {
            // Do something with value2
        } catch(e) {
            // ...
        }
    });
});

我们需要一种新的方式,能够解除主逻辑与回调函数间的耦合(分离嵌套),并保证执行的异步性。
有两种思路:声明式、命令式。对于声明式的解决这类问题,以同步方式书写异步代码,甚至是错误捕捉,需要语言层面的解决,或者至少自己要写一个简单的编译器。我们并不需要实现一个webapp,只是以工具、库的形式存在的组件,因此只考虑在现有语法框架下,使用命令式的方式。
命令式的方法,配上链式调用,最直接的就是下面这种思路(回调之间都被拆分开)
step1().anApi(step2).anApi(step3).catchError(errorFun)

由于事件等待本身不会阻塞javascipt的运行,因此图中的step2、step3、errorFun需要被储存,等待内部合适的时候触发它们。发现了么,这类似于“发布事件,等待被订阅触发”的过程,即观察者模式(也称发布-订阅模式)。
下面用一个(简单到没啥用的)玩具代码来演示如何实现的:
// 观察者(堆栈,提供添加、触发接口)
function watch() {
    var cache = [];
    return {
        done: function(callback) {
            cache.push(callback);
        },
        resolve: function() {
            for (var i=0; i<cache.length; i++) {
                cache[i].apply(this, arguments);
            }
        }
    }
}

function somethingAsync() {
    // some code...
    var lis = watch();
    事件 = function() {
        lis.resolve();
    }
    return lis;  // 返回可以绑定订阅者的接口
}
somethingAsync().done(fn1).done(fn2);

Callback


观察者模式,可以解耦回调函数的绑定。但在这里需要定制两个功能:
1、递延。对于事件,触发的时候如果没有监听,就错过了。保存触发时的参数,添加回调时判断该参数是否已有保存值,决定是否即时调用。
2、once。回调只能被触发一次。
这里需要介绍一个概念:钩子。通过在程序不同的地方埋置钩子,可以增加不同的特性和功能支持。同样是观察者模式,根据不同的需求,需要定制不同的功能。不仅是Deferred,很多时候我们都会用到观察者模型,但是需求的功能特征不同。jQuery抽象出Callback的目的就是尽可能挖掘观察者模式的潜力,实现一个match多个case的强大的观察者模式,并且考虑了循环调用的情况,不仅可以用于Deferred,还可以复用于大部分需要借用观察者模型的其他场合,一劳永逸。比如,实现迭代器的时候,有的return false表示终止,有的却不影响,要想两种都支持,需要增加一个形参,而这里的思路是通过传入字符串参数,指定代码中钩子的状态。
在Callback中,支持memory递延(add时设置)、once单次触发后lock锁定状态(fire时设置)、unique回调去重(add时设置)、stopOnfalse(fire内遍历时判断)。采用核心+外观的形式,内部有一个基本的fire(还有一个基本的add,因为没有别的接口调用直接嵌在外部调用的add内部了),和fire、fireWith外观。增加了锁定、禁用功能。思路是通过locked=true锁定封住外部调用的fire相关接口(除了存在递延memory参数,add接口仍然可以调用内部的fire操作),通过list=”“锁定add操作。因此locked(锁定),locked+list(禁用)。
Callback在1.12版本比1.11版本真心优雅不少,语义更清晰。list代表回调列表,当调用fire遍历list回调列表时,回调函数本身可能又内部调用add或fire,需要考虑。当add时,没什么影响,只需要动态判断list.length就好,fire时,需要先把任务存在任务列表里,queue就相当于任务列表,里面存着每次fire需要使用的参数(参数都是数组形式,所以肯定不是undefined)。使用firing看标记是否属于正在fire阶段。fire的过程中会持续queue.shift()然后遍历回调。外观fire接口,可以拦截locked的情况,不会向queue中push参数。由于递延的效果,add中会涉及直接执行,为了减小复杂度,执行只通过内部fire接口,用firingIndex指定开始执行的索引位置。
[源码]
// #410,Array.prototype.indexOf 兼容,下面会用到
jQuery.inArray = function( elem, arr, i ) {
    var len;

    if ( arr ) {
        if ( var indexOf = [].indexOf ) {
            return indexOf.call( arr, elem, i );
        }

        len = arr.length;
        // x?(x?x:x):x
        i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0;

        for ( ; i < len; i++ ) {

            // Skip accessing in sparse arrays
            if ( i in arr && arr[ i ] === elem ) {
                return i;
            }
        }
    }

    return -1;
}

// #3159,能把字符串'once memory' -> {'once': true, 'memory': true}
function createOptions( options ) {
    var object = {};
    jQuery.each( options.match( /\S+/g ) || [], function( _, flag ) {
        object[ flag ] = true;
    } );
    return object;
}

// #3189,参数为空格隔开的字符串,定制需要的观察者模型
// 
// options -> 4种模式(钩子),可混合
// once:  保证回调列表只被触发一次
// memory:  能够记忆最近一次触发使用的参数,回调执行时都会使用该参数
// unique:  回调不会被重复添加
// stopOnFalse:  回调返回false中断调用
jQuery.Callbacks = function( options ) {

    // 提取模式
    options = typeof options === "string" ?
        createOptions( options ) :
        jQuery.extend( {}, options );

    var // 是否正在fire触发阶段,用来判断是外部的触发,还是回调函数内部的嵌套触发
        firing,

        // 记录上次触发时使用的参数
        memory,

        // 记录是否已经被触发过至少一次
        fired,

        // 锁定外部fire相关接口
        locked,

        // 回调列表
        list = [],

        // 多次fire调用(因为可能被嵌套调用)的调用参数列表
        queue = [],

        // 回调列表list的触发索引,也会用在指定add递延触发位置
        firingIndex = -1,

        // 内部核心fire接口
        fire = function() {

            // 若只能被触发一次,此时锁定外部fire接口
            locked = options.once;

            // 标记为已触发、且正在触发
            fired = firing = true;
            for ( ; queue.length; firingIndex = -1 ) {
                // fire参数列表取出第一项,开始遍历
                memory = queue.shift();
                // 遍历
                while ( ++firingIndex < list.length ) {

                    // 若执行后返回false,判断是否有stopOnFalse钩子,指定钩子逻辑
                    if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && options.stopOnFalse ) {

                        // queue中本参数对list的遍历到此为止,跳出
                        firingIndex = list.length;
                        // 本参数不会再有递延效果,因为有回调已经返回了false
                        memory = false;
                    }
                }
            }

            // 若无递延效果,queue中最后一个触发参数不会保留
            if ( !options.memory ) {
                memory = false;
            }
            // 结束firing阶段
            firing = false;

            // 如果锁定了(比如once),外部fire封掉了,由是否有递延指定add(会调用内部fire)是否可用,无递延就要disable掉(locked+list)
            if ( locked ) {

                // 'once memory'
                if ( memory ) {
                    list = [];

                // disable()
                } else {
                    list = "";
                }
            }
        },

        // return self
        self = {

            // 添加回调,可以是回调数组集合。支持递延触发内部fire
            add: function() {
                if ( list ) {

                    // 外部显示调用add,判断是否是递延触发时机,memory推入fire列表,重置执行索引位置(递延状态下执行过fire,才不会重置memory)
                    if ( memory && !firing ) {
                        firingIndex = list.length - 1;
                        queue.push( memory );
                    }

                    // 通过递归add,支持[fn1,[fn2,[fn3,fn4> -> fn1,fn2,fn3,fn4
                    ( function add( args ) {
                        jQuery.each( args, function( _, arg ) {
                            if ( jQuery.isFunction( arg ) ) {
                                if ( !options.unique || !self.has( arg ) ) {
                                    list.push( arg );
                                }
                            } else if ( arg && arg.length && jQuery.type( arg ) !== "string" ) {

                                // Inspect recursively
                                add( arg );
                            }
                        } );
                    } )( arguments );

                    // 递延触发
                    if ( memory && !firing ) {
                        fire();
                    }
                }
                // 链式
                return this;
            },

            // 移除回调,支持多参数。去掉所有相同回调,当回调内调用remove时,若删除项为已执行项,要修正firingIndex位置
            remove: function() {
                jQuery.each( arguments, function( _, arg ) {
                    var index;
                    // Array.prototype.indexOf 兼容方法,从index索引位匹配
                    while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
                        list.splice( index, 1 );

                        // 修正firingIndex
                        if ( index <= firingIndex ) {
                            firingIndex--;
                        }
                    }
                } );
                return this;
            },

            // 判断是否有指定回调,无参数则判断回调列表是否空
            has: function( fn ) {
                return fn ?
                    // Array.prototype.indexOf 兼容方法
                    jQuery.inArray( fn, list ) > -1 :
                    list.length > 0;
            },

            // 清空list
            empty: function() {
                // 仅在list不为""时
                if ( list ) {
                    list = [];
                }
                return this;
            },

            // 禁用。list封add,locked封外部fire接口
            disable: function() {
                locked = queue = [];
                list = memory = "";
                return this;
            },
            disabled: function() {
                return !list;
            },

            // 锁定,locked封外部fire接口,是否递延判断add是否可调用内部fire
            lock: function() {
                locked = true;
                // 无递延(每次执行完memory重置为false)或没触发过,则直接禁用
                if ( !memory ) {
                    self.disable();
                }
                return this;
            },
            locked: function() {
                return !!locked;
            },

            // 把调用参数(memory[0]为环境,memory[1]为参数数组)推入queue,制定环境调用fire
            fireWith: function( context, args ) {
                if ( !locked ) {
                    args = args || [];
                    args = [ context, args.slice ? args.slice() : args ];
                    queue.push( args );
                    if ( !firing ) {
                        fire();
                    }
                }
                return this;
            },

            // 调用者为this
            fire: function() {
                self.fireWith( this, arguments );
                return this;
            },

            // 是否触发过
            fired: function() {
                return !!fired;
            }
        };

    return self;
};

Deferred


Deferred是jQuery内部的promise实现,内部使用的是递延(参数记忆)+oncelock(状态锁定)的观察者模型。有三种状态:正常时候是”notify”(没有oncelock),成功后是”resolve”,失败后是”reject”,每种状态使用一个观察者对象。当触发成功或失败时,相反的状态被禁用,但notify状态如果被触发过则不会禁用仅仅lock锁住(仅可以add递延调用,不可以外部触发)。
jQuery的实现的特点是:随意、灵活。这也算是缺点。跟promise/A+标准反差挺大的呢。
jQuery中没有自动的错误捕捉,全靠自觉,reject状态的设置本身也不像是为了错误设置的,如果你代码写太渣,没在合适的地方捕捉并reject,错误确实捉不住。标准中的reject定位就是抛出错误,我猜这应该是大量的实践证明了除了成功主要是用于错误处理吧。而且如果真的需要处理错误,done也不能做到触发下一个promise,只有then的实现可以加工一下做到。
done/fail是直接在Callback的list列表中添加回调,同步执行,回调间不会异步等待。每个then(fun)都返回一个promise,在Callback的list列表中添加一个既执行fun、又触发then内deferred对象的回调函数,若fun返回promise对象,则在其后.done/fail( newDefer.resolve/reject ),实现异步串起回调。
Deferred也是使用了两种编程方式的雏形,一种是把deferred当做一个对象,需要的时候deferred,另一种是用它包裹函数Deferred(fun),函数内封装业务逻辑,优点是可以通过依赖注入的方式实现功能,可以减少暴露外部的接口,如果平常用的少可能一时不大得心应手。当然,由于Deferred两种编程方式都使用了,减少暴露接口的特点就没有利用了。在标准的实现中,只用了第二种方式,真正意义的隐藏了resolve/reject接口(即不是返回完整的deferred)。
[源码]
// #3384,Deferred,使用闭包式写法(非面向对象式,由于add/done接口暴露,所以是可以实现面向对象式的,原型上的then可以调用到add/done)
jQuery.Deferred = function( func ) {
    var tuples = [

            // action, add listener, listener list, final state
            [ "resolve", "done", jQuery.Callbacks( "once memory" ), "resolved" ],
            [ "reject", "fail", jQuery.Callbacks( "once memory" ), "rejected" ],
            [ "notify", "progress", jQuery.Callbacks( "memory" ) ]
        ],
        // 当前状态
        state = "pending",

        // 不含resolve/reject接口的promise
        promise = {
            state: function() {
                return state;
            },
            always: function() {
                deferred.done( arguments ).fail( arguments );
                return this;
            },

            // 注意:每个then返回一个全新deferred对象的promise
            then: function( /* fnDone, fnFail, fnProgress */ ) {
                var fns = arguments;

                // 依赖传入,新生成的deferred,返回deferred.promise()
                return jQuery.Deferred( function( newDefer ) {
                    jQuery.each( tuples, function( i, tuple ) {
                        // tuples中对应tuple的对应回调函数
                        var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ];

                        // tuples中对应tuple的对应[ 'done' | 'fail' | 'progress' ]
                        // promise[ 'done' | 'fail' | 'progress' ]在下面被遍历添加
                        deferred[ tuple[ 1 ] ]( function() {
                            var returned = fn && fn.apply( this, arguments );

                            // 返回promise或deferred对象时,异步触发newDefer对应状态
                            if ( returned && jQuery.isFunction( returned.promise ) ) {
                                returned.promise()
                                    .progress( newDefer.notify )
                                    .done( newDefer.resolve )
                                    .fail( newDefer.reject );
                            } else {

                                // 非promise对象,跟done/fail效果相当,但却是通过触发下一个promise的形式。若返回值存在,参数为返回值,否则为done/fail遍历调用的argument
                                newDefer[ tuple[ 0 ] + "With" ](
                                    this === promise ? newDefer.promise() : this,
                                    fn ? [ returned ] : arguments
                                );
                            }
                        } );
                    } );
                    fns = null;
                } ).promise();
            },

            // 无参数时,返回不含resolve/reject接口的promise对象,可循环调用
            // 有参数可扩展,生成如deferred对象
            promise: function( obj ) {
                return obj != null ? jQuery.extend( obj, promise ) : promise;
            }
        },
        deferred = {};

    // 别名,不清楚是用来兼容在什么情况[摊手]
    promise.pipe = promise.then;

    // 为promise接口添加与Callback对象交互的done(对应add)/fail/progress方法
    // 为deferred对象添加与Callback对象交互的resolve/resolveWith(对应fireWith)/reject/rejectWith
    jQuery.each( tuples, function( i, tuple ) {
        // 对应观察者模型Callback
        var list = tuple[ 2 ],
            // 对应状态
            stateString = tuple[ 3 ];

        // promise[ done | fail | progress ] = list.add
        promise[ tuple[ 1 ] ] = list.add;

        // 'resolved' 'rejected'
        if ( stateString ) {
            list.add( function() {

                // state = [ resolved | rejected ]
                state = stateString;

            // [ reject_list | resolve_list ].disable(相反观察者禁用); progress_list.lock(progress锁定)
            // ^ 按位异或,0^1 = 1,1^1 = 0,(二进制写法取不同位为1,相同位为0)
            }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
        }

        // 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;
    } );

    // 合并成最终的deferred,promise相当于deferred的一个子集。deferred.promise() -> promise
    promise.promise( deferred );

    // 执行fun,并传入生成的deferred(对第二种编程形式的支持)
    if ( func ) {
        func.call( deferred, deferred );
    }

    // 返回deferred
    return deferred;
};

when


when方法返回一个deferred的promise对象。接受多个参数,没有promise接口的参数当做resolved状态,当参数中全部变为resolved状态时,会触发when中deferred的resolve。当有一个参数变成reject,会触发deferred的reject。当有参数调用notify时,每次调用都会执行一次。除了reject是使用触发项的触发参数外,resolve和reject均使用一个参数数组触发,数组中每一项对应when中参数每一项的触发参数,对于when参数中的非promise对象,对应的触发参数就是它们自身。
when还考虑到只有一个参数,且带有promise方法时,可以直接使用该参数来触发成功操作,节省开销,因此方法开头做了这个优化。因此这种情况,直接由该对象接管。触发的参数规则的不一致,个人认为很不优雅,而且updateFun里arguments.length<=1时,也不一致。
// #3480
jQuey.when = function( subordinate /* , ..., subordinateN */ ) {
    var i = 0,
        resolveValues = slice.call( arguments ),
        length = resolveValues.length,

        // 判断是否单参数且带有promise方法
        remaining = length !== 1 ||
            ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0,

        // 新生成Deferred对象,对单参数且带有promise方法进行优化
        deferred = remaining === 1 ? subordinate : jQuery.Deferred(),

        updateFunc = function( i, contexts, values ) {
            // progress触发器、resolve触发器(根据计数器判断是否触发)
            return function( value ) {
                // 设置当前触发项的环境
                contexts[ i ] = this;
                // 设置resolve/progress对应的触发参数的数组中的该位置的参数
                values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;

                // 若触发的是progress操作
                if ( values === progressValues ) {
                    deferred.notifyWith( contexts, values );

                // 触发的是resolve。计数器减至0才会触发新defer的resolve,使用resolve对应的触发参数的数组
                } else if ( !( --remaining ) ) {
                    deferred.resolveWith( contexts, values );
                }
            };
        },

        progressValues, progressContexts, resolveContexts;

    // length为0会在if ( !remaining ){}直接调用resolve,为1时由于是参数本身,
    if ( length > 1 ) {
        // 触发时设置的参数数组
        progressValues = new Array( length );
        progressContexts = new Array( length );
        resolveContexts = new Array( length );
        for ( ; i < length; i++ ) {
            if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) {
                resolveValues[ i ].promise()
                    .progress( updateFunc( i, progressContexts, progressValues ) )
                    .done( updateFunc( i, resolveContexts, resolveValues ) )
                    .fail( deferred.reject );
            } else {
                // 遇到不带promise接口的参数计数变量-1
                --remaining;
            }
        }
    }

    // 若同步执行到此处时,已经是全resolved状态,则直接触发resolve
    if ( !remaining ) {
        deferred.resolveWith( resolveContexts, resolveValues );
    }

    return deferred.promise();
};

结尾:建议再参考es6规范总结的异步编程一节。文章开头给出了地址。
......显示全文...
    点击查看全文


上一篇文章      下一篇文章      查看所有文章
2016-03-28 21:39:56  
Web前端 最新文章
10分钟
SSM框架SSM项目源码SSM源码下载java框架整合
javascript入门
JavaScript常用对象Array(2)
8.Smarty3:模版中的内置函数
表单脚本
iTextSharp5.0页眉页脚及Asp.net预览的实现
MVC基础学习—理论篇
JavaScript
http协议中get与post区别详解
360图书馆 软件开发资料 文字转语音 购物精选 软件下载 美食菜谱 新闻资讯 电影视频 小游戏 Chinese Culture 股票 租车
生肖星座 三丰软件 视频 开发 短信 中国文化 网文精选 搜图网 美图 阅读网 多播 租车 短信 看图 日历 万年历 2018年7日历
2018-7-23 0:46:24
多播视频美女直播
↓电视,电影,美女直播,迅雷资源↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  软件世界网 --