JS 自定义事件
自定义事件:就是有别于有别于带有浏览器特定行为的事件(类似click
, mouseover
, submit
, keydown
等事件),事件名称可以随意定义,可以通过特定的方法进行添加,触发以及删除。
JS自定义事件
先看个简单的事件添加的例子:
element.addEventListener("click", function() {
// 我是临时工
});
这是个简单的为DOM元素分配事件处理函数的方法(IE 不支持),有别于:
element.onclick = function() {
// 我是临时工
};
addEventListener()
可以为元素分配多个处理函数(而非覆盖),因此,我们可以继续:
element.addEventListener("click", function() {
// 我是二代临时工
});
然后,当element
被click(点击)的时候,就会连续触发“临时工”和“二代临时工”函数。
抽象→具象→本质→数据层
自定义事件思路:我们可以定义一个数组,当添加事件的时候,我们push进去这个事件处理函数;当我们执行的时候,从头遍历这个数组中的每个事件处理函数,并执行。
当多个事件以及对应数据处理函数添加后,我们最终会得到一个类似下面数据结构的对象:
_listener = {
"click": [func1, func2],
"custom": [func3],
"defined": [func4, func5, func6]
}
因此,如果我们脱离DOM, 纯碎在数据层面自定义事件的话,我们只要以构建、遍历和删除
_listener
对象为目的即可。
函数式实现
先看看函数式的实现(只展示骨干代码):
var _listener = {};
var addEvent = function(type, fn) {
// 添加
};
var fireEvent = function(type) {
// 触发
};
var removeEvent = function(type, fn) {
// 删除
};
上面的代码虽然显得比较初级,但是目的亦可实现。例如:
addEvent("alert", function() {
alert("弹出!");
});
// 触发自定义alert事件
fireEvent("alert");
但是,函数式写法缺点显而易见,过多暴露在外的全局变量(全局变量是魔鬼),方法无级联等。这也是上面懒得显示完整代码的原因,略知即可。
字面量实现
众所周知,减少全局变量的方法之一就是使用全局变量(其他如闭包)。于是,我们稍作调整:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
var Event = {
_listeners: {},
// 添加
addEvent: function(type, fn) {
if (typeof this._listeners[type] === "undefined") {
this._listeners[type] = [];
}
if (typeof fn === "function") {
this._listeners[type].push(fn);
}
return this;
},
// 触发
fireEvent: function(type) {
var arrayEvent = this._listeners[type];
if (arrayEvent instanceof Array) {
for (var i=0, length=arrayEvent.length; i<length; i+=1) {
if (typeof arrayEvent[i] === "function") {
arrayEvent[i]({ type: type });
}
}
}
return this;
},
// 删除
removeEvent: function(type, fn) {
var arrayEvent = this._listeners[type];
if (typeof type === "string" && arrayEvent instanceof Array) {
if (typeof fn === "function") {
// 清除当前type类型事件下对应fn方法
for (var i=0, length=arrayEvent.length; i<length; i+=1){
if (arrayEvent[i] === fn){
this._listeners[type].splice(i, 1);
break;
}
}
} else {
// 如果仅仅参数type, 或参数fn邪魔外道,则所有type类型事件清除
delete this._listeners[type];
}
}
return this;
}
};
使用类似下面:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
Event.addEvent("alert", function() {
alert("弹出!");
});
// 触发自定义alert事件
Event.fireEvent("alert");
您可以狠狠地点击这里:JS自定义事件字面量书写demo
默认页面document
通过Event.addEvent()
绑定了两个自定义的alert
事件,因此,此时您点击页面的空白区域(非按钮与示例代码区域),就会有如下图所示的连续两个alert框:
demo页面还有两个按钮,用来清除已经绑定的alert
事件。第一个按钮清除所有alert
事件,而点击第二个按钮清除第一个alert
事件。例如我们点击第二个按钮:
清除完毕后再点击页面的空白区域, 您会发现只会弹出“第二个弹出!”字样的弹出框了。这表明,第一个绑定自定义事件被remove掉了。
字面量实现虽然减少了全局变量,但是其属性方法等都是暴露而且都是唯一的,一旦某个关键属性(如_listeners
)不小心在某事件处reset了下,则整个全局的自定义事件都会崩溃。因此,我们可以进一步改进,例如,使用原型链继承,让继承的属性(如_listeners
)即使出问题也不会影响全局。
原型模式实现
代码如下(相比上面增加了addEvents
, fireEvents
, removeEvents
多事件绑定、执行与删除方法:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
var EventTarget = function() {
this._listener = {};
};
EventTarget.prototype = {
constructor: this,
addEvent: function(type, fn) {
if (typeof type === "string" && typeof fn === "function") {
if (typeof this._listener[type] === "undefined") {
this._listener[type] = [fn];
} else {
this._listener[type].push(fn);
}
}
return this;
},
addEvents: function(obj) {
obj = typeof obj === "object"? obj : {};
var type;
for (type in obj) {
if ( type && typeof obj[type] === "function") {
this.addEvent(type, obj[type]);
}
}
return this;
},
fireEvent: function(type) {
if (type && this._listener[type]) {
var events = {
type: type,
target: this
};
for (var length = this._listener[type].length, start=0; start<length; start+=1) {
this._listener[type][start].call(this, events);
}
}
return this;
},
fireEvents: function(array) {
if (array instanceof Array) {
for (var i=0, length = array.length; i<length; i+=1) {
this.fireEvent(array[i]);
}
}
return this;
},
removeEvent: function(type, key) {
var listeners = this._listener[type];
if (listeners instanceof Array) {
if (typeof key === "function") {
for (var i=0, length=listeners.length; i<length; i+=1){
if (listeners[i] === key){
listeners.splice(i, 1);
break;
}
}
} else if (key instanceof Array) {
for (var lis=0, lenkey = key.length; lis<lenkey; lis+=1) {
this.removeEvent(type, key[lenkey]);
}
} else {
delete this._listener[type];
}
}
return this;
},
removeEvents: function(params) {
if (params instanceof Array) {
for (var i=0, length = params.length; i<length; i+=1) {
this.removeEvent(params[i]);
}
} else if (typeof params === "object") {
for (var type in params) {
this.removeEvent(type, params[type]);
}
}
return this;
}
};
啰哩吧嗦的代码直接跳过,其实上面代码跟字面量方法相比,就是增加了下面点东西:
var EventTarget = function() {
this._listener = {};
};
EventTarget.prototype = {
constructor: this,
// .. 完全就是字面量模式实现脚本
};
然后,需要实现自定义事件功能时候,先new
构造下:
var myEvents = new EventTarget();
var yourEvents = new EventTarget();
这样,即使myEvents
的事件容器_listener
跛掉,也不会污染yourEvents
中的自定义事件(_listener
安然无恙)。
您可以狠狠地点击这里:原型模式下的JS自定义事件demo
从demo右半区域的源代码展示可以看出如何使用addEvents
, fireEvents
方法同时添加和触发多个自定义事件的。
DOM自定义事件
我们平常所使用的事件基本都是与DOM元素相关的,例如点击按钮,文本输入等,这些为自带浏览器行为事件,而自定义事件与这些行为无关。例如:
element.addEventListener("alert", function() { alert("弹出!"); });
这里的alert
就属于自定义事件,后面的function
就是自定义事件函数。而这个自定义事件是直接绑定在名为element
的DOM元素上的,因此,这个称之为自定义DOM事件。
由于浏览器的差异,上面的addEventListener
在IE浏览器下混不来(attachEvent
代替),因此,为了便于规模使用,我们需要新的添加事件方法名(合并addEventListener
和attachEvent
),例如addEvent
, 并附带事件触发方法fireEvent
, 删除事件方法removeEvent
,(命名均参考自MooTools库)。
如何直接在DOM上扩展新的事件处理方法,以及执行自定义的事件呢?
如果不考虑IE6/7浏览器,我们可以直接在DOM上进行方法扩展。例如添加个addEvent
方法:
HTMLElement.prototype.addEvent = function(type, fn, capture) { var el = this; if (window.addEventListener) { el.addEventListener(type, function(e) { fn.call(el, e); }, capture); } else if (window.attachEvent) { el.attachEvent("on" + type, function(e) { fn.call(el, e); }); } };
上面代码中的HTMLElement
表示HTML元素。以一个<p>
标签元素举例,其向上寻找原型对象过程会是这样:HTMLParagraphElement.prototype
→ HTMLElement.prototype
→ Element.prototype
→ Node.prototype
→Object.prototype
→ null
。这下您应该知道HTMLElement
所处的位置了吧,上述代码HTMLElement
直接换成Element
也是可以的,但是会让其他元素(例如文本元素)也扩展addEvent
方法,有些浪费了。
这样,我们就可以使用扩展的新方法给元素添加事件了,例如一个图片元素:
elImage.addEvent("click", function() { alert("我是点击图片之后的弹出!"); });
由于IE6, IE7浏览器的DOM水平较低,无法直接进行扩展,因此,原型扩展的方法在这两个浏览器下是行不通的。要想让这两个浏览器也支持addEvent
方法,只能是页面载入时候遍历所有DOM,然后每个都直接添加addEvent
方法了。
var elAll = document.all, lenAll = elAll.length; for (var iAll=0; iAll<lenAll; iAll+=1) { elAll[iAll].addEvent = function(type, fn) { var el = this; el.attachEvent("on" + type, function(e) { fn.call(el, e); }); }; }
您可以狠狠地点击这里:基于DOM扩展自定义方法demo
点击demo页面张含韵小姐年轻时候相片,就会有该图片alt
属性值。
测试代码如下(demo页面有代码完整展示):
<img id="image" data-src="http://image.zhangxinxu.com/image/study/s/s256/mm1.jpg" alt="年轻的张含韵" /> document.getElementById("image").addEvent("click", function() { alert("这是:" + this.alt); });
只能点到为止
直接在DOM上进行事件方法扩展其实是个糟糕的做法,因此,这里我并没有对自定义事件做进一步深入探讨(这个下一部分会讲)。
基于DOM扩展缺点有:缺少标准无规律、提高冲突可能性、性能以及浏览器支持。
扩展名字任意命,很有可能就会与未来DOM浏览器本身支持的方法相互冲突;
扩展无规律,很有可能出现A和B同名不同功能的扩展而造成冲突;
IE6-7浏览器下所有扩展都要通过遍历支持,其性能开销可想而知;
另外IE8对DOM扩展的支持并不完整,例如其支持Element.prototype
,却没有HTMLElement.prototype
.
虽然我从事的站点就是基于MooTools库的,但是,我对MooTools库基于DOM扩展方法的做法是不支持的。相反,我更亲近jQuery库的做法,也就是下面要讲的“伪DOM自定义事件”。
伪DOM自定义事件
这里的“伪DOM自定义事件”是自己定义的一个名词,用来区分DOM自定义事件的。例如jQuery库,其是基于包装器(一个包含DOM元素的中间层)扩展事件的,既与DOM相关,又不直接是DOM,因此,称之为“伪DOM自定义事件”。
下面即将展示的代码目的在于学习与认识,要想实际应用可能还需要在细节上做些调整。例如,下面测试的包装器仅仅只是包裹DOM元素,并非选择器之类;$
符号未增加冲突处理,且几个重要方法都暴露在全局环境中,没有闭包保护等。
原型以及new
函数构造不是本文重点,因此,下面这个仅展示:
var $ = function(el) { return new _$(el); }; var _$ = function(el) { this.el = el; }; _$.prototype = { constructor: this, addEvent: function() { // ... }, fireEvent: function() { // ... }, removeEvent: function() { // ... } }
本博客为转载:http://www.zhangxinxu.com/wordpress/2012/04/js-dom%E8%87%AA%E5%AE%9A%E4%B9%89%E4%BA%8B%E4%BB%B6/