Ruby's Louvre

每天学习一点点算法

导航

javascript事件系统的发展史

一个完整的事件系统,通常存在以下三个角色:

  • 事件对象,用于储存事件的状态。
  • 事件源对象,当前事件在操作的对象,如元素节点,文档对象,window对象,XMLHttpRequest对象等。
  • 事件监听器,当一个事件源生成一个事件对象时,它会调用相应的回调函数进行操作。在IE中,事件对象恒为全局属性window.event的分身。

在w3c没有把其DOM 模型引入网页时,netscape与微软已经逼不及待到快他们熟悉的语言中把相关的DOM模型搞进来了。这其实也怪javascript之父忙于把抄袭其他语言,忽略了自身事件系统的建设。从此世界被划分为两大阵营了。

DOM0时代,这里的DOM指w3c的DOM。双方都设计两种绑定事件的方法,无侵入式与侵入式。你可以说内联式与非内联式的区别。

侵入式,双方都一样。没有办法,那是很早就实现的。那时IE只有抄袭的份,还不敢胡来。

<input name="ruby" onclick="alert(this.nam)" />

然后是无侵入式,这估计是它们都完成了各自的DOM模型,实现对元素节点的索引机制之后的事了。比如有以下网页片断:

    
<input type="button" name="button1" value="aaaa"/>

我们必须自上而下,一步步找到此元素节点才能操作它。注意,那时没有所谓的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 跨浏览器的事件系统》系列是不一样,这里着重讲述设计一个事件系统遇到的各种各样的问题。而后者则给出具体的解决方案。

posted on 2010-04-27 17:00  司徒正美  阅读(9629)  评论(5编辑  收藏  举报