JavaScript事件——事件冒泡,事件捕获,事件绑定与解绑,事件委托

一.事件

事件是用户或浏览器自身执行的某种动作,这是我自己的理解。


二.事件流

事件流描述的是从页面中接收事件的顺序,也可理解为事件在页面中传播的顺序。

1.两种事件流模型

冒泡型事件流:事件的传播从DOM树的叶子到根。【推荐】——event bubbling
捕获型事件流:事件的传播葱DOM树的根到叶子。 ——event capturing

两种事件流模型.png
2.W3C标准模型

W3C标准采用捕获+冒泡。两种事件流都会触发DOM的所有对象,从document对象开始,也在document对象结束。

W3C标准模型.png

W3C标准则取其折中方案. W3C事件模型中发生的任何事件, 先(从其祖先元素window)开始一路向下捕获, 直到达到目标元素, 其后再次从目标元素开始冒泡.
而你作为开发者, 可以决定事件处理器是注册在捕获或者是冒泡阶段. 如果addEventListener的最后一个参数是true, 那么处理函数将在捕获阶段被触发; 否则(false), 会在冒泡阶段被触发.

3.浏览器兼容问题

一些标准的浏览器中,如Chrome、Firefox等,支持这两种方式。而事实上,准确的说,是这两种方式的混合方式。
在低版本IE及Opera下,是不支持事件捕获的。而这个特点最明显体现在事件绑定函数上。
IE、Opera的事件绑定函数是attachEvent,而Chrome等浏览器则是addEventListener,而后者比前者的参数多了一个,这个参数是一个布尔值。这个布尔值由用户决定,用户若设为true,则该绑定事件以事件捕获的形式参与,若为false则以事件冒泡的形势参与。


三.事件绑定与解绑

1.直接在HTML中事件处理(不推荐)

<input type="button" value="showClick" onclick="showClick()" />

这种方法的有很多缺点:
(1)首先,存在一个时差问题。因为用户如果会在 HTML 元素一出现在页面上就触发相应的事件,但当时的事件处理程序有可能尚不具备执行条件。假如用户在页面解析事件处理函数之前就触发事件,就会引发错误。为此,很多HTML事件处理程序都会被封装在一个 try-catch 块中,这样错误不会浮出水面,因为在浏览器有机会处理错误之前,错误就被捕获了。如下面的例子所示:
<input type="button" value="showClick" onclick="try{showClick();}catch(ex){}">
(2)这样扩展事件处理程序的作用域链在不同浏览器中会导致不同结果。不同 JavaScript 引擎遵循的标识符解析规则略有差异,很可能会在访问非限定对象成员时出错。
(3)HTML 与 JavaScript 代码紧密耦合。如果要更换事件处理程序,就要改动两个地方:HTML 代码和 JavaScript 代码。而这正是许多开发人员摒弃 HTML 事件处理程序,转而使用 JavaScript 指定事件处理程序的原因所在。

2. 直接在dom对象上注册事件名称,就是DOM0写法,所有浏览器支持
//事件绑定
document.getElementById("patty").onclick = function(e){};/
document.getElementById("patty")["onmousemover"] = function(e){};

//事件解绑
document.getElementById("patty")["onmousemover"] = null;

//阻止默认事件(默认事件行为:href=""链接,submit表单提交等)
document.getElementById("patty").onclick = function() {
    ……                         //你的代码
    return false;              //通过返回false值阻止默认事件行为
};

在这里我觉得有必要解释两点
(1)事件绑定通过.和[]的两种方式访问js对象属性的方法,[]的形式主要是为了解决属性名不是合法的标识符,比如:object.123肯定报错,但是object["123"]就避免了这个问题,与此同时,[]的写法,更加灵活,用字符串表示属性名称,可以在运行时动态绑定事件。
(2)return false 的含义不是阻止事件继续向顶层元素传播,而是阻止浏览器对事件的默认处理。 return false 只在当前函数有效,不会影响其他外部函数的执行。
总结
retrun true; 返回正确的处理结果。
return false;返回错误的处理结果,终止处理;阻止提交表单;阻止执行默认的行为。
return;把控制权返回给页面。

3.DOM2事件模型

· DOM2支持同一dom元素注册多个同种事件。
· DOM2新增了捕获和冒泡的概念。

(1)addEventListener(event.type, handle, boolean); IE8及以下不支持

事件类型没有on,第三个参数false 表示在事件第三阶段(冒泡)触发,true表示在事件第一阶段(捕获)触发。 如果handle是同一个方法,只执行一次。

var element=document.getElementById("patty");
var handler=function(){ }
//绑定事件
element.addEventListener('click', handler, false);  
//解绑事件
element.removeEventListener('click', handle, false);
//阻止默认事件
element.addEventListener("click", function(e){
    var event = e || window.event;
    ……
    event.preventDefault( );      //阻止默认事件
},false);
(2)attachEvent(event.type, handle ); IE特有,兼容IE8及以下,可添加多个事件处理程序,只支持冒泡阶段,并不属于DOM2

如果handle是同一个方法,绑定几次执行几次,这点和addEventListener不同。事件类型要加on,例如onclick而不是click

特别注意:

在 IE 中使用 attachEvent() 与DOM0和DOM2addEventListener有一主要区别:事件处理程序的作用域。在这些方法中,事件处理程序会在其所属元素的作用域内运行;在使用 attachEvent() 方法的情况下,事件处理程序会在全局作用域中运行,因此 this 等于 window。

var element=document.getElementById("patty");
var handler=function(){ }
/绑定事件
element.attachEvent('onclick', handler); 
//解绑事件,参数和绑定一样
element.detachEvent("onclick", handler);
//阻止默认事件
element.attachEvent("onclick", function(e){
    var event = e || window.event;
    ……
    event.returnValue = false;       //阻止默认事件
},false);
(3)封装事件绑定与解绑函数,兼容浏览器
// 事件绑定
function addEvent(element, eType, handler, bol) {
    if(element.addEventListener){           //如果支持addEventListener
        element.addEventListener(eType, handler bol);
    }else if(element.attachEvent){          //如果支持attachEvent
        element.attachEvent("on"+eType, handler);
    }else{                                  //否则使用兼容的onclick绑定
        element["on"+eType] = handle;
    }
}
// 事件解绑
function removeEvent(element, eType, handler, bol) {
    if(element.addEventListener){
        element.removeEventListener(eType, handler, bol);
    }else if(element.attachEvent){
        element.detachEvent("on"+eType, handler);
    }else{
        element["on"+eType] = null;
    }
}

作为一个追求完美的人,我们可以将两个函数写成函数的方法模式

//用事件冒泡方式,如果想兼容事件捕获只需要添加个bool参数
var EventUtil = {
    addHandler: function(element,type,handler) {
        if (element.addEventListener) {
            element.addEventListener(type,handler,false);
        }
        else if (element.attachEvent) {
            element.attachEvent('on'+type,handler);
        }
        else {
            element['on'+type] = handler;
        }
    },

    removeHandler: function(element,type,handler) {
        if (element.removeEventListener)
        {
            element.removeEventListener(type,handler,false);
        }
        else if(element.detachEvent) {
            element.detachEvent('on' +type,handler);
        }
        else {
            element['on'+type] = null;
        }
    }
}

//使用
var patty= document.getElementById("patty");
var handler = function(){
    console.log("Don't touch patty");
};
EventUtil.addHandler(patty, "click", handler);
EventUtil.removeHandler(patty, "click", handler);

四.终止事件冒泡

事件冒泡、事件捕获阻止:

event.stopPropagation( ); // 阻止事件的进一步传播,包括(冒泡,捕获),无参数 ,ie不支持
event.cancelBubble = true; // true 为阻止冒泡,也有浏览器不支持

方法1:利用event.stopPropagation( )和event.cancelBubble
    var patty=document.getElementById("patty").addEventListener("click",function(event){  
    var event = event||window.event;
      ……
     event.stopPropagation();  
     });  

这里提供一个阻止冒泡兼容性写法

function stopPropagation(event){
    event=window.event||event;
    if(event.stopPropagation){ 
      event.stopPropagation();
    }else{
     event.cancelBubble=true;
    }
}
//直接调用
document.getElementById("patty").addEventListener("click",function(event){  
    ......
    stopPropagation(event);
}
方法2:利用event.target和event.currentTarget,事件包含最初触发事件的节点引用 和 当前处理事件节点的引用,节点只处理自己触发的事件
document.getElementById("patty").addEventListener("click",function(event){  
    event=window.event||event;
    //IE没有event.target,有event.srcElement    
     var target = event.target||event.srcElement;
    if(target == event.currentTarget)  
       { 
          ……
       }  
    });  

方法一缺点:为了实现点击特定的元素显示对应的信息,方法一要求每个元素的子元素也必须终止事件的冒泡传递,即跟别的元素功能上强关联,这样的方法会很脆弱。
方法二缺点:方法二为每一个元素都增加了事件监听处理函数,事件的处理逻辑都很相似,即都有判断 if(target == event.currentTarget),这样存在了很大的代码冗余,现在是三个元素还好,当有10几个,上百个又该怎么办呢?

改进(利用事件委托)

让某个父节点统一处理事件,通过判断事件的发生地(即事件产生的节点),然后做出相应的处理
这里我们先了解下事件委托的概念

事件委托:

利用事件冒泡的特性,将里层的事件委托给外层事件,根据event对象的属性进行事件委托,改善性能。使用事件委托能够避免对特定的每个节点添加事件监听器;事件监听器是被添加到它们的父元素上。事件监听器会分析从子元素冒泡上来的事件,找到是哪个子元素的事件。

  window.onload = function() {  
      document.getElementById("box").addEventListener("click",eventPerformed,false);  
   }  
  function eventPerformed(event) {  
       var event = event||window.event;
       var target = e.target||e.srcElement;
      switch (target.id) {  
        case "div1":  
            alert("您好,我是div1。");  
            break;  
        case "div2":  
             alert("您好,我是div2。");  
            break;  
        }  
    } 

五.最后给大家送上一个跨浏览器的事件对象

var EventUtil={
    getEvent:function(event){
        return event||window.event;
    },
    getTarget:function(event){
        return event.target||event.srcElement;
    },
    preventDefault:function(){
        if(event.preventDefault){
            event.preventDefault();
        }else{
            event.returnValue=false;
        }
    },
    stopPropagation:function(){
        if(event.stopPropagation){
            event.stopPropagation();
        }else{
            event.cancelBubble=true;
        }
    },
    addHandler:function(element,type,handler){
        if(element.addEventListener){
            element.addEventListener(type,handler,false);
        }else if(element.attachEvent){
             element["e"+type]=function(){
              handler.call(element)
          }
            element.attachEvent("on"+type,element["e"+type]);
        }else{
            element["on"+type]=handler;
        }
    },
    removeHandler:function(element,type,handler){
        if(element.removeEventListener){
            element.removeEventListener(type,handler,false);
        }else if(element.detachEvent){
            element.detachEvent("on"+type,element["e"+type]);
            element["e"+type]=null;    
        }else{
            element["on"+type]=null;
        }
    }

  };


//使用
var patty=document.getElementById("patty");
var handler=function(event){
   var event=EventUtil.getEvent(event);
   var target=EventUtil.getTarget(event);
   alert(target.id);
   EventUtil.stopPropagation(event);
}
EventUtil.addHandler(patty,'click',handler);

好了,关于JS事件暂且先写这么多,欢迎大家指正博文中错误。谢谢观看!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容