代码改变世界

jQuery 源码细读 -- $.Callbacks

2013-12-11 13:32  straybird  阅读(319)  评论(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         }
fire 函数
 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             }
add 函数

引起我思考的是遍历回调列表时, 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能保证一个函数的执行不因为另一个函数而挂起吗?