深入研究JavaScript的事件机制

本篇开始将回顾下Javascript的事件机制。同时会从一个最小的函数开始写到最后一个具有完整功能的,强大的事件模块。为叙述方便将响应函数/回调函数/事件Listener/事件handler都称为事件handler。天台县羿壮冶金

先看看页面中添加事件的几种方式:

    1. 直接将JS代码写在HTML上

测试:Nowamagic

view source

print?

1 <div onclick="alert(‘欢迎访问Nowamagic.net‘);">Nowamagic</div>

HTML Element元素自身就拥有了很多onXXX属性,只需将JS代码赋值给其就可以了。赋值给onXXX的字符串将作为响应函数的函数体(FunctionBody)。大概这是上世纪90年代的写法,那时候直接把JS代码写在网页中很普遍,也许那时候的JS并不太重要,只是用来做做验证或一些花哨的效果而已。

    1. 定义一个函数,赋值给html元素的onXXX属性

view source

print?

1 <script type="text/javascript">
2     function clk(){}
3 </script>
4 <div onclick="clk()">Div2 Element</div>

先定义函数clk,然后赋值给onclick属性,这种方式也应该属于上世纪90年代的流行写法。比第一种方式好的是它把业务逻辑代码都封装在一个函数里了,使HTML代码与JS代码稍微有点儿分离,不至于第一种那么紧密耦合。

    1. 使用element.onXXX方式

view source

print?

1 <div id="d3">Div3 Element</div>
2 <script type="text/javascript">  
3     var d3 = document.getElementById(‘d3‘);
4     d3.onclick = function(){    }
5 </script>

这种方式也是早期的写法,但好处是可以将JS与HTML完全分离,前提是需要给HTML元素提供一个额外的id属性(或其它能获取该元素对象的方式)。

    1. 使用addEventListener或attachEvent

view source

print?

01 <div id="d4">Div4 Element</div>
02 <script type="text/javascript">
03 var d4 = document.getElementById(‘d4‘);
04 function clk(){alert(4)}
05 if(d4.addEventListener){
06 d4.addEventListener(‘click‘,clk,false);
07 }
08 if(d4.attachEvent){
09 d4.attachEvent(‘onclick‘,clk);
10 }
11 </script>

这是目前推荐的方式,较前两种方式功能更为强大,可以为元素添加多个事件handler,支持事件冒泡或捕获,前三种方式默认都是冒泡。IE6/7/8仍然没有遵循标准而使用了自己专有的attachEvent,且不支持事件捕获。

好,把方式4简单的封装下,  兼容标准浏览器及IE浏览器。注意attachEvent的第一个参数需要加上个"on",addEventListener第三个参数为false表示事件冒泡,attachEvent没有第三个参数,默认就是冒泡,没有捕获。

view source

print?

01 /**
02 *
03 * @param {Object} el HTML Element
04 * @param {Object} type 事件类型
05 * @param {Object} fn 事件handler
06 */
07 function addEvent(el, type, fn){
08     if(el.addEventListener){
09         el.addEventListener(type, fn, false);
10     }else{
11         el.attachEvent(‘on‘ + type, fn);
12     }
13 }

好,用这个工具函数添加一个给document添加一个点击事件:

view source

print?

1 function handler(){
2     alert(this);
3     alert(arguments[0]);
4 }
5 addEvent(document, ‘click‘, handler);

在Firefox等标准浏览器中,点击页面后将弹出 "[object HTMLDocument]",及handler中的this就是document自身。但在IE6/7/8中this却是window对象。这让人不爽,修改下与标准浏览器统一。

view source

print?

01 function addEvent(el, type, fn){
02     if(el.addEventListener){
03         el.addEventListener(type, fn, false);
04     }else
05         el[‘e‘ + fn] = function(){
06             fn.call(el, window.event);
07         }
08         el.attachEvent(‘on‘+type, el[‘e‘+fn]);
09     }
10 }

上面我们封装了一个addEvent,解决了IE6/7/8下事件handler中this为window的错误,并且统一了事件对象作为事件handler的第一个参数传入。

这篇把对应的删除事件的函数补上。上一篇中fn在IE6/7/8中实际上被包装了,IE6/7/8中真正的handler是el["e"+fn]。因此删除时要用到它。同时将两个方法挂在一个对象E上,add,remove分别添加和删除事件。

view source

print?

01 E = {
02     //添加事件
03     add : function(el, type, fn){
04         if(el.addEventListener){
05             el.addEventListener(type, fn, false);
06         }else{
07             el[‘e‘+fn] = function(){
08                 fn.call(el,evt);
09             }; 
10             el.attachEvent(‘on‘ + type, el[‘e‘+fn]);
11         }
12     },
13     //删除事件
14     remove : function(el, type, fn){
15         if(el.removeEventListener){
16             el.removeEventListener(type, fn, false);
17         }else if(el.detachEvent){
18             el.detachEvent(‘on‘ + type, el[‘e‘+fn]);
19         }
20     }  
21 };

可以看到,标准浏览器如IE9/Firefox/Safari/Chrome/Opera会使用addEventListener/removeEventListener添加/删除事件,IE6/7/8则使用attachEvent/detachEvent。标准浏览器中事件handler是传入的第三个参数fn,IE6/7/8中则是包装后的el["e"+fn]。

好了,已经拥有了添加,删除事件两个方法,并且解决了各浏览器下中的部分差异,现再添加一个主动触发事件的方法dispatch。该方法能模拟用户行为,如点击(click)操作等。 标准使用dispatchEvent方法,IE6/7/8则使用fireEvent方法。因为可能会出现异常,使用了try catch。

view source

print?

01 E = {
02     //添加事件
03     add : function(el, type, fn){
04         if(el.addEventListener){
05             el.addEventListener(type, fn, false);
06         }else{
07             el[‘e‘+fn] = function(){
08                 fn.call(el,window.event);
09             }; 
10             el.attachEvent(‘on‘ + type, el[‘e‘+fn]);
11         }
12     },
13     //删除事件
14     remove : function(el, type, fn){
15         if(el.removeEventListener){
16             el.removeEventListener(type, fn, false);
17         }else if(el.detachEvent){
18             el.detachEvent(‘on‘ + type, el[‘e‘+fn]);
19         }
20     },
21     //主动触发事件
22     dispatch : function(el ,type){
23         try{
24             if(el.dispatchEvent){
25                 var evt = document.createEvent(‘Event‘);
26                 evt.initEvent(type,true,true);
27                 el.dispatchEvent(evt);
28             }else if(el.fireEvent){
29                 el.fireEvent(‘on‘+type);
30             }
31         }catch(e){};
32     }
33 };

这就是整个事件模块的雏形,往后还有很多需要补充完善的地方。但对于普通的应用,这几个函数足以胜任。

上面的add有个问题,对同一类型事件添加多个hanlder时,IE6/7/8下会无序,如

view source

print?

01 <div id="d1" style="width:200px;height:200px;background:gold;"></div>
02 <script type="text/javascript">
03     var el = document.getElementById(‘d1‘);
04     function handler1(){alert(‘1‘);}
05     function handler2(){alert(‘2‘);}
06     function handler3(){alert(‘3‘);}
07     function handler4(){alert(‘4‘);}
08     function handler5(){alert(‘5‘);}
09     E.add(el, ‘click‘, handler1);
10     E.add(el, ‘click‘, handler2);
11     E.add(el, ‘click‘, handler3);
12     E.add(el, ‘click‘, handler4);
13     E.add(el, ‘click‘, handler5);
14 </script>

IE9/Firefox/Safari/Chomre/Opera会依次输出1,2,3,4,5。但IE6/7/8中则不一定。为解决所有浏览器中多个事件handler有序执行,我们需要一个队列来管理所有的handler。

这次,把所有的内部细节封装在一个匿名函数中,该函数执行完毕后返回如上一篇接口相同的方法。另外

  1. 把真正的事件handler挂在el上,即el.listeners,其为一个对象,每一个类型的事件为一个数组,如click为el.listeners["click"] = []。
  2. 所有的handler存在在对于的数组中
  3. 删除一个hanlder,将从数组中将其删除

view source

print?

01 E = function(){
02     function _isEmptyObj(obj){
03         for(var a in obj){
04             return false;
05         }
06         return true;
07     }
08     function _each(ary, callback){
09         for(var i=0,len=ary.length; i<len;){
10             callback(i, ary[i]) ? i=0 : i++;
11         }
12     }
13     function _remove(el, type){
14         var handler = el.listeners[type][‘_handler_‘];
15         el.removeEventListener ?
16             el.removeEventListener(type, handler, false) :
17             el.detachEvent(‘on‘+type, handler);
18         delete el.listeners[type];
19         if(_isEmptyObj(el.listeners)){
20             delete el.listeners;
21         }
22     }
23     // 添加事件
24     function add(el, type, fn){
25         el.listeners = el.listeners || {};
26         var listeners = el.listeners[type] = el.listeners[type] || [];
27         listeners.push(fn);
28         if(!listeners[‘_handler_‘]){
29             listeners[‘_handler_‘] = function(e){
30                 var evt = e || window.event;
31                 for(var i=0,fn; fn=listeners[i++];){
32                     fn.call(el, evt);
33                 }
34             }
35             el.addEventListener ?
36                 el.addEventListener(type, listeners[‘_handler_‘], false) :
37                 el.attachEvent(‘on‘ + type,  listeners[‘_handler_‘]);
38         }
39     }
40     // 删除事件
41     function remove(el, type, fn){
42         if(!el.listeners) return;
43         var listeners = el.listeners && el.listeners[type];
44         if(listeners) {
45             _each(listeners, function(i, f){
46                 if(f==fn){
47                     return listeners.splice(i, 1);
48                 }
49             });
50             if(listeners.length == 0){
51                 _remove(el,type);
52             }
53         }
54     }
55     //主动触发事件
56     function dispatch(el ,type){
57         try{
58             if(el.dispatchEvent){
59                 var evt = document.createEvent(‘Event‘);
60                 evt.initEvent(type,true,true);
61                 el.dispatchEvent(evt);
62             }else if(el.fireEvent){
63                 el.fireEvent(‘on‘+type);
64             }
65         }catch(e){};
66     }  
67     return {
68         add: add,
69         remove: remove,
70         dispatch: dispatch
71     };
72 }();

上面解决了IE6/7/8中同一个类型事件的多个handler执行无序的情况,为此改动也是较大的。实现几乎与前一个版本完全不同。但好处也是明显的。

有时需要添加只执行一次的事件handler,为此给add方法添加第四个参数one,one为true则该事件handler只执行一次。

view source

print?

1 <div id="d1" style="width:200px;height:200px;background:gold;"></div>
2 <script>
3 var el = document.getElementById(‘d1‘);
4 function handler(){alert(5)}
5 E.add(el, ‘click‘, handler, true);
6 </script>

再扩展下remove函数。

  1. 删除元素type类型的所有监听器(参数传el,type)
  2. 删除元素所有的监听器(仅传el)

比如当给一个el添加了3个click事件的handler,1个mouseover事件的handler

view source

print?

1 function handler1(){alert(‘1‘);}
2 function handler2(){alert(‘2‘);}
3 function handler3(){alert(‘3‘);}
4 function handler4(){alert(‘4‘);}
5 E.add(el, ‘click‘, f1);
6 E.add(el, ‘click‘, f2);
7 E.add(el, ‘click‘, f3);
8 E.add(el, ‘mouseover‘, f4);

使用以下语句将删除元素click的所有handler:E.remove(el, ‘click‘);

以下将删除元素身上所有的事件handler,包括click和mouseover:E.remove(el);

上面正式推出了我的事件模块event_v1,已经搭起了它的初始框架。或许有人要说,与众多JS库或框架相比,它还没有解决事件对象的兼容性问题。是的,我故意将此放到后续补充。因为事件对象的兼容性问题太多了,太繁琐了。

下面我将引入一个私有的_fixEvent函数,add中将调用该函数。_fixEvent将修复(或称包装)原生事件对象,返回一个标准的统一接口的事件对象。如下

view source

print?

01 function _fixEvent( evt, el ) {
02     var props = "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),
03         len   = props.length;
04     function now() {return (new Date).getTime();}
05     function returnFalse() {return false;}
06     function returnTrue() {return true;}
07     function Event( src ) {
08         this.originalEvent = src;
09         this.type = src.type;
10         this.timeStamp = now();
11     }
12     Event.prototype = {
13         preventDefault: function() {
14             this.isDefaultPrevented = returnTrue;
15             var e = this.originalEvent;
16             if( e.preventDefault ) {
17                 e.preventDefault();
18             }
19             e.returnValue = false;
20         },
21         stopPropagation: function() {
22             this.isPropagationStopped = returnTrue;
23             var e = this.originalEvent;
24             if( e.stopPropagation ) {
25                 e.stopPropagation();
26             }      
27             e.cancelBubble = true;
28         },
29         stopImmediatePropagation: function() {
30             this.isImmediatePropagationStopped = returnTrue;
31             this.stopPropagation();
32         },
33         isDefaultPrevented: returnFalse,
34         isPropagationStopped: returnFalse,
35         isImmediatePropagationStopped: returnFalse
36     };
37     var originalEvent = evt;
38     evt = new Event( originalEvent );
39    
40     for(var i = len, prop; i;) {
41         prop = props[ --i ];
42         evt[ prop ] = originalEvent[ prop ];
43     }
44     if(!evt.target) {
45         evt.target = evt.srcElement || document;
46     }
47     if( evt.target.nodeType === 3 ) {
48         evt.target = evt.target.parentNode;
49     }
50     if( !evt.relatedTarget && evt.fromElement ) {
51         evt.relatedTarget = evt.fromElement === evt.target ? evt.toElement : evt.fromElement;
52     }
53     if( evt.pageX == null && evt.clientX != null ) {
54         var doc = document.documentElement, body = document.body;
55         evt.pageX = evt.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
56         evt.pageY = evt.clientY + (doc && doc.scrollTop  || body && body.scrollTop  || 0) - (doc && doc.clientTop  || body && body.clientTop  || 0);
57     }
58     if( !evt.which && ((evt.charCode || evt.charCode === 0) ? evt.charCode : evt.keyCode) ) {
59         evt.which = evt.charCode || evt.keyCode;
60     }
61     if( !evt.metaKey && evt.ctrlKey ) {
62         evt.metaKey = evt.ctrlKey;
63     }
64     if( !evt.which && evt.button !== undefined ) {
65         evt.which = (evt.button & 1 ? 1 : ( evt.button & 2 ? 3 : ( evt.button & 4 ? 2 : 0 ) ));
66     }      
67     if(!evt.currentTarget) evt.currentTarget = el;
68     return evt;
69 }  

好了,现在你要

  1. 阻止事件默认行为,统一使用e.preventDefault()
  2. 停止冒泡,统一使用e. stopPropagation()
  3. 获取事件源,统一使用e.target
  4. ……/li>

更多的差异性,不在这一一列举了。

时间: 2024-10-10 20:30:15

深入研究JavaScript的事件机制的相关文章

javascript的事件机制

一.事件模型 IE 和 标准DOM的事件模型 IE系:冒泡方式 NETSCAPE系:捕获方式 标准DOM:先捕获再冒泡 冒泡,从触发点向外层.顶层扩散,最后到达document.window,遇到相同注册事件立即触发执行: 捕获则相反,从window.document向里收缩,一直到触发点,遇到相同注册事件立即触发执行: 有代码如下: <style type="text/css"> div { border: solid 1px red; } #s1 { padding:

JavaScript系列----事件机制

1.事件流 1.1.标准事件流 所谓的标准事件流指的的:EMCAScript标准规定事件流包含三个阶段,分别为事件捕获阶段,处于目标阶段,事件冒泡阶段. 下面是一段html代码,根据代码来说明标准事件流. <!DOCTYPE HTML> <html> <body> <div> <button>click</button> </div> </body> </html> 在上面的代码中,如果点击按钮bu

javascript的事件

原文:javascript的事件 前戏 今天在博客中看到了javascript的事件机制,就自己试试写一个简单的冒泡捕获测试,但是测试结果出乎了我的意料,主要是自己原来对事件了解不是很清楚,现在写篇博客记录下. 基础 先来看一下我在Aaron博客中截的一张图 DOM2中事件有三个阶段 事件的捕获阶段 事件的目标阶段 事件的冒泡阶段 事件的捕获阶段:是由document元素向发生事件的元素传递. 事件的目标阶段:是指在找到了发生事件的目标元素这个阶段,找到之后就会执行事件监听器.如果在目标元素上同

【移动端兼容问题研究】javascript事件机制详解(涉及移动兼容)--转

前言 javascript事件基础 事件捕获/冒泡 事件对象 事件模拟 移动端响应速度 PC与移动端鼠标事件差异 touch与click响应速度问题 结论 zepto事件机制 注册/注销事件 zepto模拟tap事件 tap事件的问题一览 点透问题 fastclick思想提升点击响应 实现原理 鬼点击 ios与android鼠标事件差异 事件捕获解决鬼点击 结语 前言 这篇博客有点长,如果你是高手请您读一读,能对其中的一些误点提出来,以免我误人子弟,并且帮助我提高 如果你是javascript菜

Javascript事件机制兼容性解决方案

原文:Javascript事件机制兼容性解决方案 本文的解决方案可以用于Javascript native对象和宿主对象(dom元素),通过以下的方式来绑定和触发事件: 或者 var input = document.getElementsByTagName('input')[0]; var form = document.getElementsByTagName('form')[0]; Evt.on(input, 'click', function(evt){ console.log('inp

JavaScript事件机制

<script type="text/javascript" src="http://runjs.cn/gist/2zmltkfa/all"></script> [前端培养-作业01]javascript事件机制 1.javascript事件模型 2.e.target与e.currentTarget是干什么的? 3.preventDefault与stopPropagation是干什么的 4.什么是dispatchEvent? 5.说一说事件代

JavaScript 详说事件机制之冒泡、捕获、传播、委托

DOM事件流(event  flow )存在三个阶段:事件捕获阶段.处于目标阶段.事件冒泡阶段. 事件捕获(dubbed  bubbling):通俗的理解就是,当鼠标点击或者触发dom事件时,浏览器会从根节点开始由外到内进行事件传播,即点击了子元素,如果父元素通过事件捕获方式注册了对应的事件的话,会先触发父元素绑定的事件. 事件冒泡(event  capturing):与事件捕获恰恰相反,事件冒泡顺序是由内到外进行事件传播,直到根节点. 无论是事件捕获还是事件冒泡,它们都有一个共同的行为,就是事

javascript事件机制了解与深入

一.事件的捕获与冒泡 “DOM2级事件”规定的事件流包括三个阶段:事件捕获阶段.处于目标阶段和事件冒泡阶段.下面这个图能够很形象的解释(理解捕获和冒泡必不可少的图) 按照图我们编写了代码去验证下, <div id="parent"> <div id="child"> child </div> </div> <script type="text/javascript"> var p = d

[解惑]JavaScript事件机制

[解惑]JavaScript事件机制 初学 JS 的童鞋经常会有诸多疑问,我在很多 QQ 群也混了好几年了,耳濡目染也也收获了不少,以后会总结下问题的结论,顺便说说相关知识的扩展~ 如果贸然回答还会冒泡,这不太好的,稍微严谨点考虑 0级 DOM 事件模型的话,这个答案是否定的.但是在 2级 DOM 事件模型中,答案是肯定的,这个问题值得探讨记录下. 本文地址:http://www.cnblogs.com/hustskyking/p/problem-javascript-event.html 一.