javascript事件系统的发展史
一个完整的事件系统,通常存在以下三个角色:
- 事件对象,用于储存事件的状态。
- 事件源对象,当前事件在操作的对象,如元素节点,文档对象,window对象,XMLHttpRequest对象等。
- 事件监听器,当一个事件源生成一个事件对象时,它会调用相应的回调函数进行操作。在IE中,事件对象恒为全局属性window.event的分身。
在w3c没有把其DOM 模型引入网页时,netscape与微软已经逼不及待到快他们熟悉的语言中把相关的DOM模型搞进来了。这其实也怪javascript之父忙于把抄袭其他语言,忽略了自身事件系统的建设。从此世界被划分为两大阵营了。
DOM0时代,这里的DOM指w3c的DOM。双方都设计两种绑定事件的方法,无侵入式与侵入式。你可以说内联式与非内联式的区别。
侵入式,双方都一样。没有办法,那是很早就实现的。那时IE只有抄袭的份,还不敢胡来。
<input name="ruby" onclick="alert(this.nam)" />
然后是无侵入式,这估计是它们都完成了各自的DOM模型,实现对元素节点的索引机制之后的事了。比如有以下网页片断:
我们必须自上而下,一步步找到此元素节点才能操作它。注意,那时没有所谓的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 跨浏览器的事件系统》系列是不一样,这里着重讲述设计一个事件系统遇到的各种各样的问题。而后者则给出具体的解决方案。