jQuery 源码解析(八) 异步队列模块 Callbacks 回调函数详解
异步队列用于实现异步任务和回调函数的解耦,为ajax模块、队列模块、ready事件提供基础功能,包含三个部分:query.callbacks(flags)、jquery.deferred(funct)和jquery.when()。本节讲解callbacks,也就是回调函数列表
回调函数用于管理一组回调函数,支持添加、移除、触发、锁定和禁用回调函数,为jquery.ajax、jquery.deferred()和ready()事件提供基础功能,我们也可以基于它编写新的组件。
使用方法:$.callbacks(flags),flags是一个参数,用于指定传递的标记,可以为空,可以设置为以下四个选项之一,或者任意组合也可以,如下:
unique 确保一个回调函数只能被添加一次
stoponflase 当某个回调函数返回false时中断执行
once 确保回调函数列表只能被触发一次
memory 记录上一次触发回调函数列表时的参数,之后添加的任何函数都将用记录的参数值立即调用
执行成功后返回一个对象,该函数含有如下几个方法:
add(fn/arr) 添加一个/多个回调函数到list数组中
remove(fn1,fn2,fn3...) 从list中移除多个回调函数
empty() 清空list数组
disable() 禁用列表,使他不再做任何事情,该操作不可还原
disabled() 判断是否已禁用列表,如果已经禁用了则返回true
lock() 锁定memory模式下的回调函数的上下文和参数
locked() 判断回调函数列表是否已被锁定
firewith(content,args) 以content为上下文,args为上下文,执行所有函数列表
fire(arguments) 指定上下文为当前回调函数列表来调用firewith
fired() 通过检测变量memory的值来判断回调函数列表是否被触发过
这些方法归纳起来就是新增/移除/触发回调函数,还有几个是查看当前的状态的(是否已触发、是否被禁用等)
$.callbacks()可以传入任意组合,也可以为空,例如:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>document</title> <script src="http://libs.baidu.com/jquery/1.11.1/jquery.min.js"></script> </head> <body> <script> function fn1(val){console.log('fn1 says:' + val);} function fn2(val){console.log('fn2 says ' + val);} function fn3(val){console.log('fn3 says ' + val);} var cbs = $.callbacks(); //创建callbacks对象 cbs.add([fn1,fn2,fn3]); //添加函数到回调函数列表中 cbs.fire('test1'); //触发回调函数,参数是test1 输出:fn1 says:test1、fn2 says:test1和fn3 says:test cbs.remove(fn1,fn3); //移除回调函数fn1,fn3 cbs.fire('test2'); //触发回调函数,输出:fn2 says test2 </script> </body> </html>
输出如下:
这里我们定义了一个callback(),没有传入任何参数,比较常用的是once和memory的组合,例如:
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>document</title> <script src="http://libs.baidu.com/jquery/1.11.1/jquery.min.js"></script> </head> <body> <script> function fn1(val){console.log('fn1 says:' + val);} function fn2(val){console.log('fn2 says ' + val);} function fn3(val){console.log('fn3 says ' + val);} var cbs = $.callbacks('once memory'); cbs.add([fn1,fn2]); cbs.fire('test'); //输出:fn1 says:test和fn2 says:test cbs.fire('test'); //没有输出,因为设置了once标志,当调用cbs.fire('test')后就把list清空了 cbs.add(fn3); </script> </body> </html>
输出如下:
如果传入once+memory的组合,这时回调函数被触发后再调用fire()去触发时是不会执行回调函数了的,因为当第一次fire()触发回调函数后,如果由once标记就把内部的list设为了undefined,可以理解为把list给禁用了,加入了memory标记的话当执行fire()时jquery内部会把当时的上下文和参数保存起来,这样下次直接添加回调函数就会自动回掉函数了
源码分析
writer by:大沙漠 qq:22969969
$.callbacks()的实现原理很简单,callbacks是jquery内部的一个函数,第一次执行时该函数会把传入的标记给缓存起来,通过作用域保存在内部的一个变量中,之后调用add()添加函数时也会一个个的缓存起来,最后调用fire()触发回调函数时会遍历这些回调函数列表,一个个的去触发,中间根据不同的标记做不同的处理。
$.callbacks是直接定义在内部的jquery上的,大致如下
jquery.callbacks = function( flags ) { //在jquery上添加一个callbacks方法 // convert flags from string-formatted to object-formatted // (we check in cache first) flags = flags ? ( flagscache[ flags ] || createflags( flags ) ) : {}; //先尝试从缓存对象flagscache中获取标记字符串flags对应的标记对象。如果没找到再调用工具函数createflags()创建标记 var // actual callback list list = [], // stack of fire calls for repeatable lists stack = [], // last fire value (for non-forgettable lists) memory, // 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, // add one or several callbacks to the list add = function( args ) { /*add 是一个工具函数,用于添加回调函数*/ }, // fire callbacks fire = function( context, args ) { /*fire也是一个工具函数,用于触发回调函数列表*/ }, // actual callbacks object self = { /*对象的定义*/ }; return self; //最后返回self,这是一个对象,该对象内定义的属性也就是对外的接口,供我们使用的 };
createflags用于将字符串格式的标记转换为对象格式的标记,如下:
function createflags( flags ) { //将字符串格式的标记转换为对象格式的标记 var object = flagscache[ flags ] = {}, //初始化object和flagscache[flags]为空对象 i, length; flags = flags.split( /\s+/ ); //对flags用空格分隔 for ( i = 0, length = flags.length; i < length; i++ ) { //遍历每个标记 object[ flags[i] ] = true; //属性值一律设为true,这里访问对象用方括号表示法,可以用变量来表示对象的属性。 } return object; }
以上面的第二个例子为例(once+memory),执行到这里返回后对应的如下:
然后返回内部的self对象,这样$.callbacks()就执行完毕了,后面我们调用add添加回调函数时会执行self内的add函数,如下:
add: function() { //添加回调函数 if ( list ) { var length = list.length; add( arguments ); //用工具函数add添加回调函数 // 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, unless previous // firing was halted (stoponfalse) } else if ( memory && memory !== true ) { firingstart = length; fire( memory[ 0 ], memory[ 1 ] ); } } return this; },
这里红色标记的add是在self对象同作用域的add,这是一个工具函数,如下:
add = function( args ) { //添加一个或多个回调函数到数组list中 var i, length, elem, type, actual; for ( i = 0, length = args.length; i < length; i++ ) { //遍历参数 elem = args[ i ]; //第一个参数的内容 type = jquery.type( elem ); //第一个参数的类型 ;是array或者function if ( type === "array" ) { //如果参数是数组 // inspect recursively add( elem ); //则递归调用自身 } else if ( type === "function" ) { //如果参数是函数 // add if not in unique mode and callback is not in if ( !flags.unique || !self.has( elem ) ) { //如果不是unique模式(该模式一个函数只能添加一次,就是回调函数列表中没有重复值),或者 是unique模式但未添加过该函数。 list.push( elem ); //添加args[i]到数组list中 } } } },
最后会push到list中,也就是上一层作用域的list中,例子里执行到这里list中的数据如下:
这样三个函数都被缓存起来了,最后调用fire()触发回调函数时会执行self内的fire()函数,如下:
fire: function() { self.firewith( this, arguments ); //指定上下文为当前回调函数列表来调用firewith(context, args) return this; }
fire()会将当前this作为参数,直接调用firewith,firewith如下:
firewith: function( context, args ) { //使用指定的上下文和参数触发回调函数列表中的所有回调函数 if ( stack ) { if ( firing ) { //如果回调函数正在执行当中 if ( !flags.once ) { stack.push( [ context, args ] ); } } else if ( !( flags.once && memory ) ) { //如果回调函数未在执行中,并且不是已经触发过的once模式, fire( context, args ); //调用工具函数fire(context, args )执行所有函数 } } return this; },
最后会调用fire,也就是self同作用域的工具fire函数,如下:
fire = function( context, args ) { //使用指定的上下文context和参数args调用数组list中的回调函数 args = args || []; memory = !flags.memory || [ context, args ]; //如果当前不是memory模式,则设置memory为ture,表示当前函数回调函数列表被触发过。如果当前回调函数是memory模式,设置momory为[context,args],除了表示当前函数回调函数列表被触发过,还能保存上下文和参数。 firing = true; //把firing设为ture,表示回调函数正在执行当中 firingindex = firingstart || 0; firingstart = 0; firinglength = list.length; for ( ; list && firingindex < firinglength; firingindex++ ) { //如果没有禁用列表则循环执行每个函数 if ( list[ firingindex ].apply( context, args ) === false && flags.stoponfalse ) { //执行list数组里的每一个函数,如果开启了stoponfalse标记且有一个回调函数返回false memory = true; // mark as halted //则把memory设置为ture(清除上下文),且退出余下所有函数列表的执行。 break; } } firing = false; if ( list ) { //如果没有禁用列表 if ( !flags.once ) { //如果不是once模式,即可多次触发回调函数列表 if ( stack && stack.length ) { memory = stack.shift(); self.firewith( memory[ 0 ], memory[ 1 ] ); } } else if ( memory === true ) { //如果(是once模式,而且不是memory模式) 或者 (是once+memory 且设置了stoponflase模式,并且某个回调函数返回了false) self.disable(); //则禁用回调函数列表 } else { //如果是once模式+memory模式 list = []; //则清空数组list,后续添加的回调函数还会立即执行。 } } },
fire()执行完后回调函数列表就执行完毕了,中间通过一些标记做处理,如果由传入memory,则会保存上下文,下次通过add添加回调函数时会立即执行的