jQuery源码分析(八): 回调机制基础

前端语法/样式/布局 2016-11-05

起步

上一篇介绍了jq通过func.call()来实现函数的回调。改变上下文环境来做到灵活自如的使用。

理解回调

百科里面是这么解释的:

在计算机程序设计中,回调函数,或简称回调(Callback 即call then back 被主函数调用运算后会返回主函数),是指通过函数参数传递到其它代码的,某一块可执行代码的引用。这一设计允许了底层代码调用在高层定义的子程序

例子:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void sig(int signum)
{
        printf("Received signal number %d!\n", signum);
}

int main(int argc, char *argv[])
{
        signal(SIGUSR1, sig);

        pause();

        return 0;
}

上面都是维基百科的内容。回调函数就是通过一个函数指针调用的函数。如果把函数的指针作为参数传递给另一个函数,当这个指针调用它所指向的函数时,就是我们所说的回调函数

回调的使用

回调函数在js中往往是在完成某一些动作之后执行的函数,看下面的例子:

var test = function(callback) {
    //一些动作
    callback();
};

test(function() {
    //回调要用到的动作
});

上面的例子属于同步回调,目的是在test代码执行完成后执行回调callback。jq中也是很多的回调设计,而且大多都是异步回调:

//事件回调
$(document).ready(callback);

//执行完回调
$('#cc').animate({}, 3000, callback);

//异步请求回调
$.ajax({}).done(callback);

异步指的是不需要等待这个函数执行完才执行一下个语句,在js中,异步的实现可以简单使用setTimeout()来操作:

var test = function(callback) {
    //一些动作
    setTimeout(function() {
        callback()
    }, 25)
};

test(function() {
    //回调要用到的动作
});
//other code...

这样就是就算回调函数里做了很耗时的操作,也不会耽误other code的代码执行。而同步执行是会造成阻塞的。

jq提供的回调工具:$.Callbacks

在大概在3000多行,jq提供了一种方式:

/*
 * Create a callback list using the following parameters:
 *
 */
jQuery.Callbacks = function( options ) {};

这段工具占200行代码,不太依赖于其他模块,提供一些模式:

  • once:回调列表只执行一次
  • memory:保存以前的值,将添加到这个列表的后面的最新的值立即执行调用任何回调
  • unique:确保一次只添加一次回调(回调列表里没有重复的)
  • stopOnFalse:当有一个回调函数return false;时中断回调列表的执行。
var callbacks = $.Callbacks('once');

  callbacks.add(function() {
    alert('a');
  })

  callbacks.add(function() {
    alert('b');
  })

  callbacks.fire(); //输出结果: 'a' 'b'
  callbacks.fire(); //未执行

是不是便捷很多了,代码又很清晰,所以它是一个多用途的回调函数列表对象,提供了一种强大的方法来管理回调函数队列。

jQuery.Callbacks = function( options ) {
    options = typeof options === "string" ?
        ( optionsCache[ options ] || createOptions( options ) ) :
        jQuery.extend( {}, options );
    var list = [];
    var self = {
        add: function() {
                if ( list ) {
                    //code...
                }
                return this;
            },
    };

    return self;
};

可以看到每次调用jQuery.Callbacks返回都是一个崭新的对象,回调函数列表的自然是新的,Options参数缓存,这里用到了一个技巧,保存回调函数的列表list你不能直接操作它,jq用很巧妙的方法把他弄成类似面向对象的私有变量一样,用户访问不到list:

callbacks.add()        回调列表中添加一个回调或回调的集合。
callbacks.disable()    禁用回调列表中的回调
callbacks.disabled()   确定回调列表是否已被禁用。 
callbacks.empty()      从列表中删除所有的回调.
callbacks.fire()       用给定的参数调用所有的回调
callbacks.fired()      访问给定的上下文和参数列表中的所有回调。 
callbacks.fireWith()   访问给定的上下文和参数列表中的所有回调。
callbacks.has()        确定列表中是否提供一个回调
callbacks.lock()       锁定当前状态的回调列表。
callbacks.locked()     确定回调列表是否已被锁定。
callbacks.remove()     从回调列表中的删除一个回调或回调集合。

深入理解jq的回调

add方法

add: function() {
    if ( list ) {
        // First, we save the current length
        var start = list.length;
        (function add( args ) {
            jQuery.each( args, function( _, arg ) {
                var type = jQuery.type( arg );
                if ( type === "function" ) {
                    if ( !options.unique || !self.has( arg ) ) {
                        list.push( arg );
                    }
                } else if ( arg && arg.length && type !== "string" ) {
                    // Inspect recursively
                    add( arg );
                }
            });
        })( arguments );
        // Do we need to add the callbacks to the
        // current firing batch?
        if ( firing ) {
            firingLength = list.length;
        // With memory, if we're not firing then
        // we should call right away
        } else if ( memory ) {
            firingStart = start;
            fire( memory );
        }
    }
    return this;
},

可以看到使用的语法是callbacks.add( Function | Array),最后都是会通过list.push(arg)加到回调列表中。

fire方法

执行回调的fire()方法存在一个调用链:self.fire –> self.fireWith –> fire最后执行的是"私有方法"

fire = function( data ) {
    memory = options.memory && data;
    fired = true;
    firingIndex = firingStart || 0;
    firingStart = 0;
    firingLength = list.length;
    firing = true;
    for ( ; list && firingIndex < firingLength; firingIndex++ ) {
        if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) {
            memory = false; // To prevent further calls using add
            break;
        }
    }
    firing = false;
    if ( list ) {
        if ( stack ) {
            if ( stack.length ) {
                fire( stack.shift() );
            }
        } else if ( memory ) {
            list = [];
        } else {
            self.disable();
        }
    }
},

执行调用函数:list[ firingIndex ].apply( data[ 0 ], data[ 1 ] )将存在列表中的函数取出来,通过apply调用。data[0]可以定义回调函数使用的上下文环境。其中,fire()的上下文是self,fireWith( context, args )才能自定义上下文。通过判断回调函数的返回值来终止回调列表的执行,将控制权交给用户。

回调模式

模式有once memory unique stopOnFalse四种。

once模式

once的作用确保回调列表只执行(.fire())一次。var a = $.Callbacks('once');jQuery.Callbacks中有个控制是否回调函数可重复使用的stack = !options.once && [],会被赋值为false。可以推出会执行self.disable();禁用回调列表中的回调:

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

noce模式下出发fire后就会清理list,回调列表失去引用也会被销毁。a.add(function)也变得无效了。

memory模式

保持以前的值,fire执行后不会清除回调列表,并且如果是fire()执行后再add的时候会直接触发fire,从而再次执行list列表(从新加的位置开始回调)。我还揣测不出这样有什么好处,能应用在什么场景。

add:function() {
    //code....
    if ( firing ) {
        firingLength = list.length;
    } else if ( memory ) {
        firingStart = start;
        fire( memory );
    }
}

就是说,在fire()函数执行前,变量firing和memory都是undefined,fire()执行后再add的时候会直接触发fire(memory)。此时如果再执行a.fire(),会从回调列表的最前头开始回调.

var a = $.Callbacks('memory');
var fn1 = function (i) {
    console.log(i);

};
a.add(fn1);
a.fire(1);
a.add(fn1);
a.fire(2);
a.add(fn1);
a.fire(3);
//输出 1 1 2 2 2 3 3 3

unique模式

确保一次只能添加一个回调(所以在列表中没有重复的回调):if ( !options.unique || !self.has( arg ) ) {list.push( arg );}:

has: function( fn ) {
    return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length );
},

stopOnFalse模式

当一个回调函数返回 false 时中断调用.

这四种模式可以混合使用:var a = $.Callbacks('once stopOnFalse unique')

总结

回调函数就是一个通过函数指针调用的函数。js可以通过 call() 或者 apply() 来回调并进行上下文环境替换。回调是一种设计模式。


本文由 hongweipeng 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

如果对您有用,您的支持将鼓励我继续创作!