jQuery 源码细读 -- $.Callbacks
2013-12-11 13:32 straybird 阅读(326) 评论(0) 编辑 收藏 举报$.Callbacks 是 jQuery 提供的可以方便地处理各种回调(callback)列表的类,其源代码是闭包的经典实现。
基本原理就是通过在闭包环境内保存一个 list = [] 数组用于存储回调列表,并用 firing,firingStart,firingLength,firingIndex等标志位来控制闭包的有序执行,下面是最重要的2个内部函数,触发函数 fire 和 添加函数 add。
1 fire = function (data) { 2 memory = options.memory && data; 3 fired = true; 4 firingIndex = firingStart || 0; 5 firingStart = 0; 6 firingLength = list.length; 7 firing = true; 8 for (; list && firingIndex < firingLength; firingIndex++) { 9 if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { 10 memory = false; // To prevent further calls using add 11 break; 12 } 13 } 14 firing = false; 15 if ( list ) { 16 if ( stack ) { 17 if ( stack.length ) { 18 fire( stack.shift() ); 19 } 20 } else if ( memory ) { 21 list = []; 22 } else { 23 self.disable(); 24 } 25 } 26 }
1 add: function () { 2 if ( list ) { 3 // First, we save the current length 4 var start = list.length; 5 (function add( args ) { 6 jQuery.each( args, function( _, arg ) { 7 var type = jQuery.type( arg ); 8 if ( type === "function" ) { 9 if ( !options.unique || !self.has( arg ) ) { 10 list.push( arg ); 11 } 12 } else if ( arg && arg.length && type !== "string" ) { 13 // Inspect recursively 14 add( arg ); 15 } 16 }); 17 })( arguments ); 18 // Do we need to add the callbacks to the 19 // current firing batch? 20 21 if (firing) { 22 console.log('firing'); 23 firingLength = list.length; 24 // With memory, if we're not firing then 25 // we should call right away 26 } else if ( memory ) { 27 firingStart = start; 28 fire( memory ); 29 } 30 } 31 return this; 32 }
引起我思考的是遍历回调列表时, firing 标志位的使用,遍历前赋值 firing = true,遍历完赋值 firing = false。在 add 函数内如果检查到 firing === true,则将回调函数加入到还没遍历完的列表末端即可。
可是问题来了,什么情况下会出现for 循环没执行完的情况下 add 函数被调用呢?即 add 函数执行时 firing 有没可能为 true?
我在 add 函数碰上 firing === true 时加上一句调试 console.log('firing')
1. 第一种情况,在某个回调函数内部再嵌套添加另一个回调。代码如下:
var callBacks = $.Callbacks(); callBacks.add(function () { console.log('callBacks 0'); callBacks.add(function () { console.log('callBacks 2'); }); }); callBacks.add(function () { console.log('callBacks 1'); }); callBacks.fire(); //执行结果 //callBacks 0 //firing //callBacks 1 //callBacks 2
还有没其他的可能呢?js 不是单线程的吗?是不是只要在每个回调函数内不再调用 callBacks.add 就不会碰上 firing === true 呢?
2. 利用浏览器对页面事件的响应处理
<input id="text1" type="text" /> <script type="text/javascript"> var text1 = document.getElementById('text1'); text1.onblur = function () { console.log('.onblur() is called'); callBacks.add(function () { console.log('callBacks 2'); }); }; text1.focus(); var callBacks = $.Callbacks(''); callBacks.add(function () { console.log('callBacks 0'); text1.blur(); console.log('.blur() is called'); }); callBacks.add(function () { console.log('callBacks 1'); }); callBacks.fire(); //IE下执行结果 //callBacks 0 //.blur() is called //callBacks 1 //.blur() is called //非IE浏览器下执行结果 //callBacks 0 //.onblur() is called //firing //.blur() is called //callBacks 1 //callBacks 2 </script>
这就是浏览器处理事件流程时带来的 js 执行流混乱。
在非 IE 下 .blur() 的调用会使当前执行堆栈挂起,转而执行 onblur 事件的回调函数,等 onblur处理完了才回头执行 .blur() 后面的代码。
IE则会等 .blur() 所在的上下文执行完之后才执行 onblur 事件的回调函数。
总结就是,浏览器无法保证 JavaScript 的单线程线性执行。详细可以看看这篇文章
Is javascript guaranteed to be single-threaded?
里面还讲到一个有趣的点,不要认为 alert 函数会挂起这个js 执行,window.onresize 事件在这种情况下还是可以被触发的,Linux 很容易, window 下可以通过改变分辨率的方式做到。
最后,我开始思考另一个问题,脱离了浏览器的 nodeJs 会出现这种问题吗?nodeJs能保证一个函数的执行不因为另一个函数而挂起吗?