zoukankan      html  css  js  c++  java
  • jQuery-1.9.1源码分析系列(五) 回调对象

      jQuery.Callbacks()提供的回调函数队列管理本来是延时回调处理的一部分,但是后面将其独立出来作为一个模块。jQuery就是这样,各个模块间的代码耦合度是处理的比较好的,值得学习。虽然是从延时回调处理中独立出来的,但是它的功能非常强大,提供了一种强大的方法来管理回调函数队列。

      大家都明白封装函数的目的:去耦合与简化操作

      通常情况下函数队列的处理方式

    //执行函数
    function runList(arr){
        for(var i = 0; i < arr.length; i++){
            arr[i]();    
        }
      arr.length = 0; }
    var list = []; //添加函数队列 list[list.length] = function(){alert(1)}; list[list.length] = function(){alert(2)};
    list[list.length] = function(){alert(3)};
    //执行 
    runList(list);//三个函数顺序执行

      使用$.callbacks封装以后的处理为

    var callbacks = $.Callbacks("unique");
    
    callbacks.add( function(){alert(1)} );
    callbacks.add( function(){alert(2)} );
    callbacks.add( function(){alert(3)} );
    //执行
    callbacks.fire();//三个函数顺序执行

      干净了很多。而且代码可读性比最开始的那个要好很多。list[list.length]神马的最讨厌了。还有主要的是$.callbacks有四个属性可以组合,这个组合可就很强大了。

      

    a. Callbacks的四个可设置的属性分析


     

    once: 确保这个回调列表只执行( .fire() )一次(像一个递延 Deferred).

           设置“once”在执行第一次fire后会直接禁用该Callbacks(fire函数代码段else {self.disable();})

    var f1 = function(value) { console.log(value); };
    var callbacks = $.Callbacks('once');
    
    callbacks.add(f1);//无执行结果,添加一个回调
    callbacks.fire(1);//执行结果1。清除回调列表
    callbacks.add(f1);//没有添加回调直接返回
    callbacks.fire(2);//无执行结果
    callbacks.add(f1);//没有添加回调直接返回
    callbacks.fire(3);//无执行结果

    memory: 保持以前的值(参数),将函数添加到这个列表的后面,并使用先前保存的参数立即执行该函数。 内部变量会保存上次执行的场景。

      他有一个特点,就是在第一次fire之前使用add添加的回调都不会马上执行,只有调用了一次fire之后使用add添加的回调会马上执行。该设置本身不会清除之前的回调列表。

      需要注意的是每次add内部执行fire函数都会将firingStart置为0,只有下次add的时候会从新设置firingStart的值。

      eg:

    var f1 = function(value) {    console.log(value); };
    var callbacks = $.Callbacks("memory");
     
    callbacks.add( fn1 );//无执行结果
    callbacks.fire( "1" );//执行结果1。保存场景参数1
     
    callbacks.add( fn1 );//执行结果1。使用上次保存的场景参数1
    callbacks.fire( "2" );//执行结果2,2。保存场景参数2
     
    callbacks.add( fn1 );//执行结果2。使用上次保存的场景参数2
    callbacks.fire( "3" );//执行结果3,3,3。保存场景参数3
     
    callbacks.add( fn1 );//执行结果3。使用上次保存的场景参数3
    callbacks.fire( "4" );//执行结果4,4,4,4。保存场景参数4

      组合使用,组合使用中间使用空格隔开

      设置“once memory”, options.once=options.memory=true。在执行第一次fire后会把回到列表清空,而且之后每次add马上执行后页同样会把回调列表清空(fire函数代码段else if ( memory ) {list = [];})。

           eg:

    var f1 = function(value) { console.log(value); };
    var callbacks = $.Callbacks('once memory');
    
    callbacks.add(f1);//无执行结果,添加一个回调
    callbacks.fire(1);//执行结果1。清除回调列表,保存场景参数1
    callbacks.add(f1);//添加一个回调并执行结果1,使用上次保存的场景参数。清除回调列表
    callbacks.fire(2);//无执行结果
    callbacks.add(f1);//添加一个回调并执行结果1,使用上次保存的场景参数。清除回调列表
    callbacks.fire(3);//无执行结果

      两个设置之间用空格,不支持其他符号,比如设置“once,memory”等同于没有设置。      

      eg:

    var f1 = function(value) { console.log(value); };
    var callbacks = $.Callbacks('once,memory');
    
    callbacks.add(f1);//无执行结果,添加一个回调
    callbacks.fire(1);//执行结果1
    callbacks.add(f1);//添加一个回调
    callbacks.fire(2);//执行结果2,2
    callbacks.add(f1);//添加一个回调
    callbacks.fire(3);//执行结果3,3,3
    callbacks.add(f1);//添加一个回调
    callbacks.fire(4);//执行结果4,4,4,4

     

    unique: 确保一次只能添加一个回调(所以在列表中没有重复的回调).

    stopOnFalse: 当一个回调返回false 时中断调用

           当有一个回调返回false的时候,会设置memory为false。导致memory失去作用(后续add的函数不会马上执行,当然先前memory保证了前面执行过得函数不再执行这也条也就不起作用了。下次fire会从回调列表的第一个开始执行)。

    b. 整体结构


      使用缓存是jQuery中最常见的技巧。$.Callbacks中也不例外。主要是缓存Callbacks中遇到的选项(字符串)。

    // 使用过的选项缓存
    var optionsCache = {};
    
    // 新增和缓存回调设置于optionsCache中
    function createOptions( options ) {
        var object = optionsCache[ options ] = {};
        jQuery.each( options.match( core_rnotwhite ) || [], function( _, flag ) {
            object[ flag ] = true;
        });
        return object;
    }
    
    jQuery.Callbacks = function( options ) {
        // 尽可能读取缓存,没有则新增缓存
        options = typeof options === "string" ?
        ( optionsCache[ options ] || createOptions( options ) ) :
        jQuery.extend( {}, options );
    
        var // 回调列表正在执行(为true的时候)的标志
            firing,
            // 最后执行的值(为memory选项下保存)
            memory,
            // 回调已经被执行过的标志
            fired,
            // 循环执行回调列表的结束位置
            firingLength,
            // 当前真正执行的回调的索引值 (执行下个回调的时候回更改【如果必要的话】)
            firingIndex,
            // 循环执行回调列表的开始位置(在函数add和fireWith中使用)
            firingStart,
            // 回调列表
            list = [],
            // Stack记录要重复执行的回调列表
            stack = !options.once && [],
            // data数组一般第一个元素是上下文环境,第二个元素是参数
            //执行回调列表
            fire = function( data ) {…},
            // 回调对象
            self = {
                // 添加回调
                add: function() {…},
                // 移除回调
                remove: function() {…},
                ...
                // 给定 context 和 arguments执行所有回调
                fireWith: function( context, args ) {
                    args = args || [];
                    //组装args,第一个元素为上下文环境,第二个元素为参数列表
                    args = [ context, args.slice ? args.slice() : args ];
                    //有list且函数列表没有被执行过或者存在要循环执行的函数列表
                    if ( list && ( !fired || stack ) ) {
                        //如果正在fire,则把函数场景记录在stack中
                        if ( firing ) {
                            stack.push( args );
                        //否则,至此那个fire
                        } else {
                            fire( args );
                        }
                    }
                    return this;
                },
                // 使用给定的arguments执行所有回调
                fire: function() {
                    self.fireWith( this, arguments );
                    return this;
                },
                ...
            };
        return self;
    };

      

      下面分析两个最重要的两个函数,添加回调函数add和执行回调函数fire

    c. add:添加回调


      添加回调函数比较简单,针对可能传递的值(函数或者函数数组)将回调添加到回调列表中即可,这里使用了一个闭包,使用了外部变量list。

            (function add( args ) {
                jQuery.each( args, function( _, arg ) {
                    var type = jQuery.type( arg );
                    if ( type === "function" ) {
                        //当$.Callbacks('unique')时,保证列表里面不会出现重复的回调
                        if ( !options.unique || !self.has( arg ) ) {
                            list.push( arg );
                        }
                    //如果是数组则递归添加
                    } else if ( arg && arg.length && type !== "string" ) {
                        add( arg );
                    }
                });
            })( arguments );

      但是这里需要对用户初始化设置的属性做一些特殊的处理。

      如果列表没有定义或null(一般只有在用户设置once且执行过一次后list才会白置为未定义),直接返回list

        //如果列表没有定义或null(一般只有在用户设置once且执行过一次后list才会白置为未定义)
        if ( list ) {
            ...
        }
        return this;

      当有回调真正执行的时候,需要重新设定回调列表的结束位置firingLength,使后续添加的函数也会执行。实际上这个功能很受争议,不过正常情况一般不会出现添加函数的时候正在执行某个回调。

      还有一个比较重要的判断:对于设置了'memory'选项并fire过了回调列表,并且没有还在等待中的回调要fire,则应当马上执行新添加的回调(执行fire(memory))

            // 如果正在fire,则设定要执行结束的点firingLength,使后续添加的函数最后不会执行
            if ( firing ) {
                firingLength = list.length;
            // 对于memory(设置了'memory' option并fire过了,memory才能通过该else if语句),
            //如果没有回调真正fire,应当马上执行fire(memory)。
            } else if ( memory ) {
                //这里保证了前面执行过得函数不再执行
                firingStart = start;
                fire( memory );
            }

      完整的源码如下

    add: function() {
        //如果列表没有定义或null(一般只有在用户设置once且执行过一次后list才会白置为未定义)
        if ( list ) {
            // 保存当前list长度,为memory处理备用
            var start = list.length;
            (function add( args ) {
                jQuery.each( args, function( _, arg ) {
                    var type = jQuery.type( arg );
                    if ( type === "function" ) {
                        //当$.Callbacks('unique')时,保证列表里面不会出现重复的回调
                        if ( !options.unique || !self.has( arg ) ) {
                            list.push( arg );
                        }
                    //如果是数组则递归添加
                    } else if ( arg && arg.length && type !== "string" ) {
                        add( arg );
                    }
                });
            })( arguments );
            // 如果正在fire,则设定要执行结束的点firingLength,使后续添加的函数最后执行
            if ( firing ) {
                firingLength = list.length;
            // 对于memory(设置了'memory' option并fire过了,memory才能通过该else if语句),
            //如果我们后续没有fire,应当马上执行fire(memory)。
            } else if ( memory ) {
                //这里保证了前面执行过得函数不再执行
                firingStart = start;
                fire( memory );
            }
        }
        return this;
    }
    View Code

      

    d. fire函数详解


      该函数执行回调,最终执行代码段为

    if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) {
           memory = false; // 阻止未来可能由于add所产生的回调
           break;
    }
    fire = function( data ) {
      //有memory才给memory赋值当前场景data
      memory = options.memory && data;
      fired = true;
      firingIndex = firingStart || 0;
      //每次fire后都会重置成0,下次$.callbacks.fire调用都会从0开始。当然设置为‘memory’使用add函数内部fire会设置firingStart的值导致回调函数列表执行起始位置更改
      firingStart = 0;
      firingLength = list.length;
      firing = true;
      //函数开始执行从firingStart到firingLength的所有函数   for ( ; list && firingIndex < firingLength; firingIndex++ ) {     //执行firingIndex对应的函数,如果设置是遇到false返回就停止,则设置memory,阻止后续函数执行     if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) {       memory = false; // 阻止未来可能由于add所产生的回调       break;     }   }   //标记回调结束   firing = false;   //如果列表存在   if ( list ) {     //如果堆栈存在(一般没有设置once的时候都进入该分支)     if ( stack ) {       //如果堆栈不为空       if ( stack.length ) {         //执行stack中第一个元素         fire( stack.shift() );       }     //如果有记忆,则清空列表(在设置为once且memory的时候会进入到此分支)     } else if ( memory ) {       list = [];     //禁用回调,该callbacks将不可用,将list/stack/memory都设为未定义     } else {       self.disable();     }   } },

      真正重要的是执行完成回调以后的处理

      //如果列表存在
      if ( list ) {
        //如果堆栈存在(一般没有设置once的时候都进入该分支)
        if ( stack ) {
          //如果堆栈不为空
          if ( stack.length ) {
            //执行stack中第一个元素
            fire( stack.shift() );
          }
        //如果有记忆,则清空列表(在设置为once且memory的时候会进入到此分支)
        } else if ( memory ) {
          list = [];
        //禁用回调,该callbacks将不可用,将list/stack/memory都设为未定义
        } else {
          self.disable();
        }
      }
    View Code

      首先看最外层的判断

    if ( list ){
        ...
    }

      什么时候会进不了这个分支呢?唯有当self.disable()被调用的时候,下一次fire就进入不了这个分支。查看self.disable源码

                    disable: function() {
                        list = stack = memory = undefined;
                        return this;
                    }

      根据里面的判断唯有当options选项有once,并且选项中没有memory或选项中有stopOnFalse且执行的回调返回false。这个时候回进入到里面的分支直接将整个回调禁用掉。

        //禁用回调,该callbacks将不可用,将list/stack/memory都设为未定义
        } else {
          self.disable();
        }

      第一个内部分支if ( stack )主要是选项中没有once就进入。

      第二个内部分支只有在选项至少有once和memory的时候才会进入。当然,如果还有stopOnFalse且执行的回调返回false会进入到第三个分支。

        //如果有记忆,则清空列表(在设置为once且memory的时候会进入到此分支)
        } else if ( memory ) {

      好了,这个jQuery.Callbacks就到这里。需要注意的就是多个选项混合使用要特别小心。

  • 相关阅读:
    P4097 [HEOI2013]Segment 李超线段树
    P3592 [POI2015]MYJ
    P3698 [CQOI2017]小Q的棋盘
    P4098 [HEOI2013]ALO 可持久化01Trie
    P2331 [SCOI2005]最大子矩阵
    P4099 [HEOI2013]SAO
    loj #6032. 「雅礼集训 2017 Day2」水箱 线段树优化DP转移
    CF765F Souvenirs 离线+线段树+主席树
    CF1097D Makoto and a Blackboard
    loj #6570. 毛毛虫计数
  • 原文地址:https://www.cnblogs.com/chuaWeb/p/jQuery-1-9-1-Callbacks.html
Copyright © 2011-2022 走看看