在 js 开发中,由于没有多线程,经常会遇到回调这个概念,比如说,在 ready 函数中注册回调函数,注册元素的事件处理等等。在比较复杂的场景下,当一个事件发生的时候,可能需要同时执行多个回调方法,可以直接考虑到的实现就是实现一个队列,将所有事件触发时需要回调的函数都加入到这个队列中保存起来,当事件触发的时候,从这个队列重依次取出保存的函数并执行。

可以如下简单的实现。

首先,实现一个类函数来表示这个回调类。在 javascript 中,使用数组来表示这个队列。

function Callbacks() {
this.list = [];
}

然后,通过原型实现类中的方法。增加和删除的函数都保存在数组中,fire 的时候,可以提供参数,这个参数将会被传递给每个回调函数。

Callbacks.prototype = {
add: function(fn) {
this.list.push(fn);
},
remove: function(fn){
var position = this.list.indexOf(fn);
if( position >=0){
this.list.splice(position, 1);
}
},
fire: function(args){
for(var i=0; i<this.list.length; i++){
var fn = this.list[i];
fn(args);
}
}
};

测试代码如下:

function fn1(args){
console.log("fn1: " + args);
} function fn2(args){
console.log("fn2: " + args);
} var callbacks = new Callbacks();
callbacks.add(fn1);
callbacks.fire("Alice"); callbacks.add(fn2);
callbacks.fire("Tom"); callbacks.remove(fn1);
callbacks.fire("Grace");

或者,不使用原型,直接通过闭包来实现。

function Callbacks() {

    var list = [];

    return {
add: function(fn) {
list.push(fn);
}, remove: function(fn){
var position = list.indexOf(fn);
if( position >=0){
list.splice(position, 1);
}
}, fire: function(args) {
for(var i=0; i<list.length; i++){
var fn = list[i];
fn(args);
}
}
};
}

这样的话,示例代码也需要调整一下。我们直接对用 Callbacks 函数就可以了。

function fn1(args){
console.log("fn1: " + args);
} function fn2(args){
console.log("fn2: " + args);
} var callbacks = Callbacks();
callbacks.add(fn1);
callbacks.fire("Alice"); callbacks.add(fn2);
callbacks.fire("Tom"); callbacks.remove(fn1);
callbacks.fire("Grace");

下面我们使用第二种方式继续进行。

对于更加复杂的场景来说,我们需要只能 fire 一次,以后即使调用了 fire ,也不再生效了。

比如说,可能在创建对象的时候,成为这样的形式。这里使用 once 表示仅仅能够 fire 一次。

var callbacks = Callbacks("once");

那么,我们的代码也需要进行一下调整。其实很简单,如果设置了 once,那么,在 fire 之后,将原来的队列中直接干掉就可以了。

function Callbacks(options) {
var once = options === "once";
var list = []; return {
add: function(fn) {
if(list){
list.push(fn);
}
}, remove: function(fn){
if(list){
var position = list.indexOf(fn);
if( position >=0){
list.splice(position, 1);
}
}
}, fire: function(args) {
if(list)
{
for(var i=0; i<list.length; i++){
var fn = list[i];
fn(args);
}
}
if( once ){
list = undefined;
}
}
};
}

jQuery 中,不只提供了 once 一种方式,而是提供了四种类型的不同方式:

  • once: 只能够触发一次。
  • memory: 当队列已经触发之后,再添加进来的函数就会直接被调用,不需要再触发一次。
  • unique: 保证函数的唯一
  • stopOnFalse: 只要有一个回调返回 false,就中断后继的调用。

这四种方式可以组合,使用空格分隔传入构造函数即可,比如 $.Callbacks("once memory unique");

官方文档中,提供了一些使用的示例。

callbacks.add(fn1, [fn2, fn3,...])//添加一个/多个回调
callbacks.remove(fn1, [fn2, fn3,...])//移除一个/多个回调
callbacks.fire(args)//触发回调,将args传递给fn1/fn2/fn3……
callbacks.fireWith(context, args)//指定上下文context然后触发回调
callbacks.lock()//锁住队列当前的触发状态
callbacks.disable()//禁掉管理器,也就是所有的fire都不生效

由于构造函数串实际上是一个字符串,所以,我们需要先分析这个串,构建为一个方便使用的对象。

// String to Object options format cache
var optionsCache = {}; // Convert String-formatted options into Object-formatted ones and store in cache
function createOptions( options ) {
var object = optionsCache[ options ] = {};
jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) {
object[ flag ] = true;
});
return object;
}

这个函数将如下的参数 "once memory unique" 转换为一个对象。

{
once: true,
memory: true,
unique: true
}

再考虑一些特殊的情况,比如在 fire 处理队列中,某个函数又在队列中添加了一个回调函数,或者,在队列中又删除了某个回调函数。为了处理这种情况,我们可以在遍历整个队列的过程中,记录下当前处理的起始下标、当前处理的位置等信息,这样,我们就可以处理类似并发的这种状况了。

// Flag to know if list was already fired
fired,
// Flag to know if list is currently firing
firing,
// First callback to fire (used internally by add and fireWith)
firingStart,
// End of the loop when firing
firingLength,
// Index of currently firing callback (modified by remove if needed)
firingIndex,
// Actual callback list
list = [],

如果在 fire 处理过程中,某个函数又调用了 fire 来触发事件呢?

我们可以将这个嵌套的事件先保存起来,等到当前的回调序列处理完成之后,再检查被保存的事件,继续完成处理。显然,使用队列是处理这种状况的理想数据结构,如果遇到这种状况,我们就将事件数据入队,待处理的时候,依次出队数据进行处理。什么时候需要这种处理呢?显然不是在 once 的状况下。在 JavaScript 中,堆队列也是通过数组来实现的,push 用来将数据追加到数组的最后,而 shift 用来出队,从数组的最前面获取数据。

不过,jQuery 没有称为队列,而是称为了 stack.

// Stack of fire calls for repeatable lists
stack = !options.once && [],

入队代码。

if ( firing ) {
stack.push( args );
}

出队代码。

if ( list ) {
if ( stack ) {
if ( stack.length ) {
fire( stack.shift() );
}
} else if ( memory ) {
list = [];
} else {
self.disable();
}
}

先把基本结构定义出来,函数的开始定义我们使用的变量。

jQuery.Callbacks = function( options ) {
var options = createOptions(options); var
memory, // Flag to know if list was already fired
// 是否已经 fire 过
fired,
// Flag to know if list is currently firing
// 当前是否还处于 firing 过程中
firing,
// First callback to fire (used internally by add and fireWith)
// fire 调用的起始下标
firingStart, // End of the loop when firing
// 需要 fire 调用的队列长度
firingLength, // Index of currently firing callback (modified by remove if needed)
// 当前正在 firing 的回调在队列的索引
firingIndex, // Actual callback list
// 回调队列
list = [], // Stack of fire calls for repeatable lists
// 如果不是 once 的,stack 实际上是一个队列,用来保存嵌套事件 fire 所需的上下文跟参数
stack = !options.once && [], _fire = function( data ) {
}; var self = {
add : function(){},
remove : function(){},
has : function(){},
empty : function(){},
fireWith : function(context, args){
_fire([context, args]);
};
fire : function(args){
this.fireWith(this, args);
}
/* other function */
}
return self;
};

其中的 stack 用来保存在 fire 之后添加进来的函数。

而 firingIndex, firingLength 则用来保证在调用函数的过程中,我们还可以对这个队列进行操作。实现并发的处理。

我们从 add 函数开始。

add: function() {
if ( list ) { // 如果使用了 once,在触发完成之后,就不能再添加回调了。
// 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? 正在 firing 中,队列长度发生了变化
if ( firing ) {
firingLength = list.length;
// With memory, if we're not firing then
// we should call right away 如果是 memory 状态,而且已经触发过了,直接触发, memory 是保存了上次触发的状态
} else if ( memory ) {
firingStart = start;
fire( memory );
}
}
return this;
},

删除就简单一些了。检查准备删除的函数是否在队列中,while 的作用是一个回调可能被多次添加到队列中。

// Remove a callback from the list
remove: function() {
if ( list ) {
jQuery.each( arguments, function( _, arg ) {
var index;
while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
list.splice( index, 1 );
// Handle firing indexes
if ( firing ) {
if ( index <= firingLength ) {
firingLength--;
}
if ( index <= firingIndex ) {
firingIndex--;
}
}
}
});
}
return this;
},

has, empty, disable, disabled 比较简单。

// Check if a given callback is in the list.
// If no argument is given, return whether or not list has callbacks attached.
has: function( fn ) {
return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length );
},
// Remove all callbacks from the list
empty: function() {
list = [];
firingLength = 0;
return this;
},
// Have the list do nothing anymore
disable: function() {
list = stack = memory = undefined;
return this;
},
// Is it disabled?
disabled: function() {
return !list;
},

锁住的意思其实是不允许再触发事件,stack 本身也用来表示是否禁止再触发事件。所以,通过直接将 stack 设置为 undefined,就关闭了再次触发事件的可能。

// Lock the list in its current state
lock: function() {
stack = undefined;
if ( !memory ) {
self.disable();
}
return this;
},
// Is it locked?
locked: function() {
return !stack;
},

fire 是暴露的触发方法。fireWith 则可以指定当前的上下文,也就是回调函数中使用的 this 。第一行的 if 判断中表示了触发事件的条件,必须存在 list,必须有 stack 或者还没有触发过。

// Call all callbacks with the given context and arguments
fireWith: function( context, args ) {
if ( list && ( !fired || stack ) ) {
args = args || [];
args = [ context, args.slice ? args.slice() : args ];
if ( firing ) {
stack.push( args );
} else {
fire( args );
}
}
return this;
},
// Call all the callbacks with the given arguments
fire: function() {
self.fireWith( this, arguments );
return this;
},
// To know if the callbacks have already been called at least once
fired: function() {
return !!fired;
}
};

真正的 fire  函数。

// Fire callbacks
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();
}
}
},

jQuery-2.1.3.js 中的 Callback 实现。

/*
* Create a callback list using the following parameters:
*
* options: an optional list of space-separated options that will change how
* the callback list behaves or a more traditional option object
*
* By default a callback list will act like an event callback list and can be
* "fired" multiple times.
*
* Possible options:
*
* once: will ensure the callback list can only be fired once (like a Deferred)
*
* memory: will keep track of previous values and will call any callback added
* after the list has been fired right away with the latest "memorized"
* values (like a Deferred)
*
* unique: will ensure a callback can only be added once (no duplicate in the list)
*
* stopOnFalse: interrupt callings when a callback returns false
*
*/
jQuery.Callbacks = function( options ) { // Convert options from String-formatted to Object-formatted if needed
// (we check in cache first)
options = typeof options === "string" ?
( optionsCache[ options ] || createOptions( options ) ) :
jQuery.extend( {}, options ); var // Last fire value (for non-forgettable lists)
memory,
// Flag to know if list was already fired
fired,
// Flag to know if list is currently firing
firing,
// First callback to fire (used internally by add and fireWith)
firingStart,
// End of the loop when firing
firingLength,
// Index of currently firing callback (modified by remove if needed)
firingIndex,
// Actual callback list
list = [],
// Stack of fire calls for repeatable lists
stack = !options.once && [],
// Fire callbacks
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();
}
}
},
// Actual Callbacks object
self = {
// Add a callback or a collection of callbacks to the list
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;
},
// Remove a callback from the list
remove: function() {
if ( list ) {
jQuery.each( arguments, function( _, arg ) {
var index;
while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
list.splice( index, 1 );
// Handle firing indexes
if ( firing ) {
if ( index <= firingLength ) {
firingLength--;
}
if ( index <= firingIndex ) {
firingIndex--;
}
}
}
});
}
return this;
},
// Check if a given callback is in the list.
// If no argument is given, return whether or not list has callbacks attached.
has: function( fn ) {
return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length );
},
// Remove all callbacks from the list
empty: function() {
list = [];
firingLength = 0;
return this;
},
// Have the list do nothing anymore
disable: function() {
list = stack = memory = undefined;
return this;
},
// Is it disabled?
disabled: function() {
return !list;
},
// Lock the list in its current state
lock: function() {
stack = undefined;
if ( !memory ) {
self.disable();
}
return this;
},
// Is it locked?
locked: function() {
return !stack;
},
// Call all callbacks with the given context and arguments
fireWith: function( context, args ) {
if ( list && ( !fired || stack ) ) {
args = args || [];
args = [ context, args.slice ? args.slice() : args ];
if ( firing ) {
stack.push( args );
} else {
fire( args );
}
}
return this;
},
// Call all the callbacks with the given arguments
fire: function() {
self.fireWith( this, arguments );
return this;
},
// To know if the callbacks have already been called at least once
fired: function() {
return !!fired;
}
}; return self;
};

jQuery 之 Callback 实现的更多相关文章

  1. jquery html() callback

    通过JQuery的.html()函数我们可以非常方便地加载一段HTML到指定的元素中,例如给<div></div>中放入一组图片.问题是JQuery的.html()函数是同步的 ...

  2. jQuery 常用速查

    jQuery 速查 基础 $("css 选择器") 选择元素,创建jquery对象 $("html字符串") 创建jquery对象 $(callback) $( ...

  3. jQuery初级篇(一)

    知识说明: jQuery库是一个javascript库文件,它比起javascript来,写的更少,但做得更多,下面便对刚开始学习jQuery,一些基础知识整理出来,供后期翻阅. 一.      jQ ...

  4. 动态加载(异步加载)jquery/MUI类库 页面加载完成后加载js类库

    动态加载Mui类库: // ==UserScript== // @name // @version 1.4.0 // @author zzdhidden@gmail.com // @namespace ...

  5. jquery 原理

    /* * my-jquery-1.0 *//* * 网上也有很多实现的版本,不过这是我在我自己的理解下写的,加上注释,希望可以解释清楚.*//* * 整个jquery包含在一个匿名函数中,专业点叫闭包 ...

  6. Jquery EasyUI封装简化操作

    //confirm function Confirm(msg, control) { $.messager.confirm('确认', msg, function (r) { if (r) { eva ...

  7. jquery源码分析

    作者:zuoxiaolong8810(左潇龙),转载请注明出处,特别说明:本博文来自博主原博客,为保证新博客中博文的完整性,特复制到此留存,如需转载请注明新博客地址即可. 前段时间上班无聊之时,研究了 ...

  8. 一篇介绍jquery很好的

    本文基于jQuery1.7.1版本,是对官方API的整理和总结,完整的官方API见http://api.jquery.com/browser/ 0.总述 jQuery框架提供了很多方法,但大致上可以分 ...

  9. 26 个 jQuery使用技巧

    1. 禁用右键点击(Disable right-click) $(document).ready(function(){ $(document).bind("contextmenu" ...

随机推荐

  1. 每日Scrum(9)

    今天我们小组进行了软件的测试和界面的美化,特别是在主界面美化方面下了一些功夫,找了很多图片,把格式也处理的很完美,符合界面的一个框架,看起来,美观多了,至此,软件的beta版是基本完成了.

  2. 第二题 已知有十六支男子足球队参加2008 北京奥运会。写一个程序,把这16 支球队随机分为4 个组。采用List集合和随机数 2008 北京奥运会男足参赛国家: 科特迪瓦,阿根廷,澳大利亚,塞尔维亚,荷兰,尼日利亚、日本,美国,中国,新西 兰,巴西,比利时,韩国,喀麦隆,洪都拉斯,意大利

    package com.hanqi.test; import java.util.ArrayList; import java.util.List; import java.util.Random; ...

  3. 记一次MongoDB Map&Reduce入门操作

    需求说明 用Map&Reduce计算几个班级中,每个班级10岁和20岁之间学生的数量: 需求分析 学生表的字段: db.students.insert({classid:1, age:14, ...

  4. sql server 有关锁的视图说明 syslockinfo

    ransact-SQL 参考   https://msdn.microsoft.com/zh-cn/library/ms179881.aspx  syslockinfo 包含有关所有已授权.正在转换和 ...

  5. PostgreSql常用脚本

    添加表字段 ALTER TABLE public.university ADD COLUMN "Province" character varying(10); COMMENT O ...

  6. Android setTag()/getTag()-(转)

    (转自:http://www.cnblogs.com/topcoderliu/archive/2011/06/07/2074419.html) [自己经历:要getTag前提是要有setTag] se ...

  7. 通过url 下载文件

    1.问题简介 通过文件的url,将文件下载到本地.文件存储的位置为:tomcat服务器的文件夹(通过读取properties文件:可看:http://www.cnblogs.com/0201zcr/p ...

  8. hadoop2.6.2分布式环境搭建

    1.准备三台机器,机器名是:master.slave01.slave02 1.1 最小化安装centos6.5 1.2 安装ssh,yum -y install openssh-clients(这一步 ...

  9. CSS background-position 用法详细介绍

    语法: background-position : length || length background-position : position || position 取值: length  : ...

  10. oracle操作之传输表空间

    一.传输表空间概述 什么是传输表空间,传输表空间技术始于oracle9i,不论是数据字典管理的表空间还是本地管理的表空间,都可以使用传输表空间技术:传输表空间不需要在源数据库和目标数据库之间具有同样的 ...