YUI自定义事件订阅者的作用域问题

前几天, 同事在使用YAHOO.util.Event.onDOMReady时, 回调函数得不到执行, 浏览器提示回调函数”is not a function”(可以确定回调函数是存在且有效的), 经检查, 发现是作用域’惹的祸’, 这里重现一下场景, 大伙在使用时注意一下.

一. 示例代码

// 基于YUI 2.7或2.8
YAHOO.namespace('Demo');
YAHOO.Demo = function() {
     var age = 24;
     return {
         init: function() {
            var curAge = this.showAge(); // 浏览器会提示this.showAge is not a function
            alert(curAge);
         },
         showAge: function(curAge) {
              return YAHOO.lang.isNumber(curAge) ? curAge : age;
         }
     };
}();
// 会出错的调用方式
YAHOO.util.Event.onDOMReady(YAHOO.Demo.init);
// 用匿名函数将回调包起来, 则可以成功执行
YAHOO.util.Event.onDOMReady(function() {YAHOO.Demo.init();});

二. 分析

1. 首先, 我看了一下onDOMReady的代码:

onDOMReady: function(fn, obj, overrideContext) {
	if (this.DOMReady) {
		setTimeout(function() {
			var s = window;
			if (overrideContext) {
				if (overrideContext === true) {
					s = obj;
				} else {
					s = overrideContext;
				}
			}
			fn.call(s, "DOMReady", [], obj);
		}, 0);
	} else {
		this.DOMReadyEvent.subscribe(fn, obj, overrideContext);
	}
}

我在调用YAHOO.util.Event.onDOMReady(YAHOO.Demo.init);时, DOM还不可用, 看来还得继续看一下DOMReadyEvent事件的定义代码.

2. DOMReadyEvent事件的定义代码:

if (!YAHOO.util.Event) {
     YAHOO.util.Event = function() {
           ...
           return {
               ...
               DOMReadyEvent: new YAHOO.util.CustomEvent("DOMReady", this),
               ...
           }
      }();
}

YAHOO.util.CustomEvent共接受4个参数, 第2个参数即为回调函数的默认作用域, 此时, this指向的是window, 看到这里, 我推测回调被执行时, 作用域被改变了, 于是看了一下YAHOO.util.CustomEvent中fire方法的源码.

3. fire方法的定义代码:

// 这里列出一些关键部分的代码
fire: function() {
     ...
     var len=this.subscribers.length;
     ...
     var subs = this.subscribers.slice(); // this.subscribers用于存储当前事件(如DOMReadyEvent)的所有事件订阅者(即回调函数)
     for (i=0; i<len; ++i) {
          var s = subs[i];
          ...
          var scope = s.getScope(this.scope); // this.scope表示回调函数的默认作用域
          if (this.signature == YAHOO.util.CustomEvent.FLAT) {
              try {
                  ret = s.fn.call(scope, param, s.obj);
              } catch(e) { ... }
          } else {
              try {
                  ret = s.fn.call(scope, this.type, args, s.obj);
              } catch(e) { ... }
          }
      }
}

看到这里, 可以确定回调被执行时, 作用域被改成了scope, 而这个scope的具体值是什么, 则需要看一下getScope方法的源码咯.

4. getScope方法的定义代码:

YAHOO.util.Subscriber.prototype.getScope = function(defaultScope) {
    if (this.overrideContext) {
        if (this.overrideContext === true) {
            return this.obj;
        } else {
            return this.overrideContext;
        }
    }
    return defaultScope;
};

从这里可以看出, scope的值取决于obj和overrideContext.

三. 总结

DOMReadyEvent在定义时, 指定回调函数的默认作用域为window, 而同事在添加回调时, 采用YAHOO.util.Event.onDOMReady(YAHOO.Demo.init);的形式, 没有传递后两个参数(obj, overrideContext), 使得回调函数执行时的作用域变成了window, 这样, this.showAge变成了window.showAge(即this指向了window), 而根本不存在名为showAge的全局函数, 故而浏览器会提示”this.showAge is not a function”.

使用YAHOO.util.Event.onDOMReady(function() {YAHOO.Demo.init();})的形式, init方法的作用域没有被改变(被改变作用域的是那个匿名函数), 故而能成功执行.

所以, 大家在给自定义事件添加回调时, 要注意指定正确的作用域, 如果不能确定作用域, 我建议使用匿名函数包起来(推荐这种方式, 这样不需要管作用域的问题).

附: YUI 2.8中, 不知道出于什么样的考虑, 将DOMReadyEvent的定义改成了这样: DOMReadyEvent: new YAHOO.util.CustomEvent(“DOMReady”, YAHOO, 0, 0, 1), 但即使是这样, 作用域的问题依然存在.

, , , ,

Leave a comment!