YUI2.8自定义事件的一个小bug

2010-3-7

问题

YUI2.8的自定义事件如果添加了两个相同的侦听函数,在移除这些侦听函数时会有错误:

var testEvent = new YAHOO.util.CustomEvent("testEvent");
testEvent.subscribe(eventHandler);
testEvent.subscribe(eventHandler);
testEvent.unsubscribe(eventHandler);
function eventHandler(){
    alert("testEvent fire");
}
testEvent.fire();
//只会显示一个"testEvent fire"

上面看似没问题,注册了两个相同的侦听函数,执行一次unsubscribe移除了一个,于是只剩一个侦听函数,但事实不是这样:

var testEvent = new YAHOO.util.CustomEvent("testEvent");
testEvent.subscribe(eventHandler);
testEvent.subscribe(eventHandler);
testEvent.subscribe(eventHandler);
testEvent.subscribe(eventHandler);
testEvent.unsubscribe(eventHandler);
function eventHandler(){
    alert("testEvent fire");
}
testEvent.fire();
//只会显示两个"testEvent fire"

注册四个相同的侦听函数,执行了一次unsubscribe,却只剩下两个侦听函数有效。

原因

看unsubscribe的源码,在移除侦听函数时用了这样一个循环:

for (var i=0, len=this.subscribers.length; i<len; ++i) {
    var s = this.subscribers[i];
    if (s && s.contains(fn, obj)) {
        this._delete(i);
        found = true;
    }
}
....
 _delete: function(index) {
    var s = this.subscribers[index];
    if (s) {
        delete s.fn;
        delete s.obj;
    }
   this.subscribers.splice(index, 1);
}

问题出在,执行this.subscribers.splice(index,1)时数组长度变了,被删除的元素位置会被下一个元素位置顶替,而for循环里的i继续叠加,就会跳过漏检查这个位置的新元素。

修正方法

1.如果想一次unsubscribe移除所有相同的侦听函数,只需把for改成倒序遍历:

for (var i=this.subscribers.length-1; i>-1; i–)

这样在删除过程中数组元素个数和位置变化也不会导致漏检查。

2.如果想让unsubscribe一次只移除一个侦听函数,应该在for里找到符合条件的元素时跳出循环:

for (var i=0, len=this.subscribers.length; i<;len; ++i) {
    var s = this.subscribers[i];
    if (s && s.contains(fn, obj)) {
        this._delete(i);
        found = true;
        break;
    }
}

不知实际应用中会不会有同时注册两个相同的侦听函数的情况,有的话也是非常少的,所以此bug没什么威胁~

评论

2010年3月12日 0:48

倒序遍历可以这么写:
var len = foo.length;
while(len–){}
效率更高。