我们想要达到的目标是,一个对象已经封装了事件处理的逻辑,而我们知道对象本身是不能捕获事件的,只有DOM元素可以捕获事件,如何动态地绑定DOM事件的处理句柄到对象实例上。看下面的代码:
var Button = function(id){
this.div = document.createElement("div");
this.div.id = id;
this.onClick = function(){
alert(this.div.id); // 这里的this是div,而不是我们想要的Button实例
};
this.div.onclick = this.onClick;
}; |
代码的意图很明显,div上的onclick由Button对象的onClick方法来处理,但我们会发现,其实这样工作是有问题的,this.onClick方法里的this其实在事件处理阶段是指向div的,而不是我们想当然的Button实例。如何让this指向Button实例呢?我们提到一个技巧, 扩展JavaScript的Function原型:
Function.prototype.bind = function(context){
var _method = this;
return function(e) {
var array = [e || window.event];
return _method.apply(context, array);
};
}; |
为一个方法绑定执行的上下文,这样就为将this指定到Button实例提供了可能,上面Button的例子可以改为:
Function.prototype.bind = function(context){
var _method = this;
return function(e) {
var array = [e || window.event];
return _method.apply(context, array);
};
};
var Button = function(id){
this.div = document.createElement("div");
this.div.id = id;
this.onClick = function(e){
alert(this.div.id); // 这时this是指向Button实例的
};
this.div.onclick = this.onClick.bind(this);
};
|
这样虽然可以工作了,但还不完美,如果我们需要在处理事件时有多个处理句柄,并且还能随时移除事件句柄,那上面的方法还需要改进,就是加入attachEvent和detachEvent了。
Function.prototype.bind = function(context){
var _method = this;
return function(e) {
var array = [e || window.event];
return _method.apply(context, array);
};
};
/**
* Attach event listener to a DOM object
*
* @param domObj
* @param evType, for example "click", "load" etc
* @param thi$, the context of handler
* @param handler, event handler
*/
$attachEvent = function(domObj, evType, thi$, handler){
var _handler = handler.bind(thi$);
if(domObj.addEventListener){
domObj.addEventListener(evType, _handler, false);
}else{
domObj.attachEvent("on"+evType, _handler);
}
return _handler;
};
/**
* Detach event listener from a DOM object
*
* @see J$VM.attachEvent(domObj, evType, thi$, handler)
*/
$detachEvent = function(domObj, evType, thi$, handler){
if(domObj.removeEventListener){
domObj.removeEventListener(evType, handler, false);
}else{
domObj.detachEvent("on"+evType, handler);
}
};
var Button = function(id){
this.div = document.createElement("div");
this.div.id = id;
this.onClick = function(e){
alert(this.div.id); // 这时this是指向Button实例的
// 假设在这个逻辑中我们需要移除事件句柄,但其实这一句也是不如我们预期的。
$detachEvent(this.div, "click", this, this.onClick);
};
$attachEvent(this.div, "click", this, this.onClick);
};
|
引入这两个方法后,再看Button的例子,好像完美了,但实际上,detachEvent(this.div, "click", this, this.onClick)这一句并不如我们预期的那样工作,因为,在attachEvent内部,我们attach的实际上是bind方法返回的方法,而不是this.onClick,所以detachEvent时,并不能找到这个this.onClick。我们还需要继续改造bind方法和detachEvent方法。
Function.prototype.bind = function(context){
var _method = this;
_method.__handler__ = function(e) {
// 在_method里暗藏一个真实的绑定句柄
var array = [e || window.event];
return _method.apply(context, array);
};
return _method.__handler__;
};
$detachEvent = function(domObj, evType, thi$, handler){
var _handler = handler.__handler__ || handler; // 取出__handler__
if(domObj.removeEventListener){
domObj.removeEventListener(evType, _handler, false);
}else{
domObj.detachEvent("on"+evType, _handler);
}
};
|
到这里,我们的事件绑定机制基本上完美了,最后附上一段完整的代码:
(function(){
window.J$VM = {};
Function.prototype.bind = function(context){
var _method = this;
_method.__handler__ = function(e) {
var array = [e || window.event];
return _method.apply(context, array);
};
return _method.__handler__;
};
/**
* Attach event listener to a DOM object
*
* @param domObj
* @param evType, for example "click", "load" etc
* @param thi$, the context of handler
* @param handler, event handler
*/
J$VM.attachEvent = function(domObj, evType, thi$, handler){
var _handler = handler.bind(thisObj);
if(domObj.addEventListener){
domObj.addEventListener(evType, _handler, false);
}else{
domObj.attachEvent("on"+evType, _handler);
}
return _handler;
};
/**
* Detach event listener from a DOM object
*
* @see J$VM.attachEvent(domObj, evType, thi$, handler)
*/
J$VM.detachEvent = function(domObj, evType, thi$, handler){
var _handler = handler.__handler__ || handler;
if(domObj.removeEventListener){
domObj.removeEventListener(evType, _handler, false);
}else{
domObj.detachEvent("on"+evType, _handler);
}
};
/**
* Cancel event bubble
*/
J$VM.cancelBubble = function(e){
if(e.stopPropagation){
e.stopPropagation();
}else{
window.event.cancelBubble = true;
}
};
})(); |
|