本文将要介绍一下javascript事件系统的详细发展史。一个完整的事件系统,通常存在以下三个角色:
在w3c没有把其DOM 模型引入网页时,netscape与微软已经逼不及待到快他们熟悉的语言中把相关的DOM模型搞进来了。这其实也怪之父忙于把抄袭其他语言,忽略了自身事件系统的建设。从此世界被划分为两大阵营了。
DOM0时代,这里的DOM指w3c的DOM。双方都设计两种绑定事件的方法,无侵入式与侵入式。你可以说内联式与非内联式的区别。
侵入式,双方都一样。没有办法,那是很早就实现的。那时IE只有抄袭的份,还不敢胡来。
<input name="ruby" onclick="alert(this.nam)" />
然后是无侵入式,这估计是它们都完成了各自的DOM模型,实现对元素节点的索引机制之后的事了。比如有以下网页片断:
<FORM name="form1">
<input
type="button"
name="button1"
value="aaaa"/>
</FORM>
我们必须自上而下,一步步找到此元素节点才能操作它。注意,那时没有所谓的document.getElementById。网景的做法,把相关绑定的代码放进一个script标签中:
<form name="form1">
<input
type="button"
name="button1"
value="aaaa"/>
</form>
如果你不想把代码用window.onload = function(){}这代码块括起来,那么你得把这script标签放于表单元素之后。
微软也有一套索引机制,基本与网景的一样,但IE4还引入了document.all与document.all.tags。不过IE还有另一套方式:
<script
for="button1"
event="onclick"
language="JavaScript">
alert("this.aaa")
</script>
不过,它用不了this(或者能,我不会),另要求一个script标签对应body中的一个标签,实在很浪费,最终被淘汰出局了。
这就是DOM0的绑定机制,另以内联方式写在标签中的代码,其实相当于以下方式:
<p id="aa" onclick="alert('aaaa')">相当于↓</p>
<script type="text/javascript">
var p = document.getElementById("aa")
p.onclick = new Function("alert('aaaa')")//相当于↓
p.onclick = function(){alert('aaaa')}
</script>
至此,事件系统三个角色都出场了。通过索引机制得到的对象(元素节点什么的),作为事件源,onclick,onmousemove之类的事件属性,它们充当监听器,onclick后面的函数就是回调函数,这是异步执行的。
随着无侵入的兴起,放到web标准中,应该叫做表现行为结构相分离。在标签内写onclick什么的应该唾弃。无侵入式编程有一种让人越写越多代码的欲望。以前总是缩在一个标签内,随时注意双引号与单引号的套嵌,写多了就烦了,不想写了,现在没有这限制,就像脱缰的马,把更多注意力用于兼容更多浏览器与创造新的点子上。好了,写着写着,人们就开始想能不能在同一个元素上绑定两个onclick事件呢?!
<script type="text/javascript">
var p = document.getElementById("aa")
p.onclick = function(){alert('第一次')}
p.onclick = function(){alert('第二次')}
</script>
当然,只能alert第二个,我们当然也可以用一些技巧达到这目的:
<p id="aa" onclick="alert('第一次')">能绑定多个同类型函数</p>
<script type="text/javascript">
var p = document.getElementById("aa")
var addEvent = function(el,type,fn) {
var type = "on"+type
var old = el[type];
if (typeof el[type] != 'function') {
el[type] = fn
}else {
el[type] = function() {
old();
fn();
}
}
}
addEvent(p,"click",function(){alert('第二次')});
addEvent(p,"click",function(){alert('第三次')});
</script>
但当要用户搞这东西是不行,因此浏览器商把它们做成内置的。顺带还搞了个事件流,也就是允许事件对象在控件间(标签)中传递。IE的一套API是createEventObject, attachEvent, dettachEvent, fireEvent,事件流是自下向上。网景那套就不清楚了,但听说w3c也是从它那一套发展而来,API比较复杂,createEvent, initEvent,addEventListener, removeEventListener dispatchEvent,那个initEvent还有许多版本呢,如initMouseEvent, initKeyEvent,参数非常多,用于更精确的配置。addEventListener拥有三个参数,但第三个参数通常只在事件代理中有用,通常为false,与IE保持一致,自下而上的冒泡。由于w3c的劣性根,总想与IE划分界线,它最高能冒泡到window(IE为document):
event = dom.Event(type);
args = [event].concat(args);
var parent = caller;
while(!event.isPropagationStopped() && parent){//isPropagationStopped为w3c dom的一个方法,
dom.events.handle.apply( parent,args); //判定是否已禁止冒泡
parent = parent.parentNode || (parent != window) && window;
}
很奇怪的是HTML的parentNode竟然是文档对象。如果是捕获就麻烦多了,这里不谈它。现在看一下多事件绑定时的兼容问题吧。比如上面那个addEvent其实够用了,DE大神的addEvent也是根据DOM0事件搞出来的。但有一些事件是DOM0绝对模拟不了,如FF的DOMMouseScroll事件,因为没有onDOMMouseScroll这个属性,它必须要用addEventListener,但IE,opera,chrome等支持的mousewheel。因此我们还是离不开这些高级的API。一个通用addEvent函数:
var addEvent = (function () {
if (document.addEventListener) {
return function (el, type, fn) {
el.addEventListener(type, fn, false);
};
} else {
return function (el, type, fn) {
el.attachEvent('on' + type, function () {
return fn.call(el, window.event);
});
}
}
})();
不过还是有问题,IE下绑定回调函数不是先进先出,详见《说明IE与非IE浏览器在事件绑定的执行顺序问题》。嗯,这些我将留在下一部分讲。
PS,这个系列与《javascript 跨浏览器的事件系统》系列是不一样,这里着重讲述设计一个事件系统遇到的各种各样的问题。而后者则给出具体的解决方案。