4.1、浏览器事件详解(DOM事件、事件捕获&事件冒泡、事件对象、事件委托;浏览器事件兼容写法)

浏览器事件模型

DOM事件

历史:
1997 年的 6 月和 10 月:Netscape Navigator 4 和 IE4 分别发布 DHTML
1998 年10 月:W3C 总结了 IE 和 Navigator4 的规范,制定了 DOMLevel 1即 DOM1,之前 IE 与 Netscape 的规范则被称为 DOMLevel 0 即 DOM0
W3C 后来将 DOM1 升级为 DOM2,DOM2级规范开始尝试以一种符合逻辑的方式来标准化 DOM事件

DOM0级事件

事件就是用户或浏览器自身执行的某种操作,如click、load、mouseover等,都是事件的名字,而响应某个事件的函数就被称为事件处理程序。

btn.onclick = function(){
   console.log('this is a click event')
}

当浏览器检测到用户点击该按钮时,执行btn.onclick.call(btn,event),btn指代this(调用者),事件对象 event,该对象也可以通过 arguments[0] 来访问,它包含了事件相关的所有信息。

btn.onclick = function(e){
   console.log('this is a click event');
   console.log(e);  //  事件对象
}
IE中DOM0级事件

但是在 IE 中,在使用 DOM0 级方法添加事件处理程序时,event 是作 window 对象的一个属性而存在的。此时访问事件对象需要通过 window.event

btn.onclick = function(){
   console.log(window.event);  //  IE中事件对象    
}

DOM1级事件

1级DOM标准中并没有定义事件相关的内容,所以没有所谓的1级DOM事件模型

DOM2级事件

  • DOM0级 可以认为 onclick 是 btn 的一个属性,DOM2级 则将属性升级为队列。
  • DOM2级事件定义了两个方法,用于处理指定和删除事件处理程序的操作,addEventListener()和removeEventListener(),所有的 DOM 节点中都包含这两个方法,它们都接收 3 个参数。
    • 1、要处理的事件名
    • 2、作为事件处理程序的函数;
    • 3、布尔值,true 代表在捕获阶段调用事件处理程序,false 表示在冒泡阶段调用事件处理程序,默认为 false;
function fn2(){
  //  do something else
}
 btn.addEventListener('click',function(){//匿名函数。
  //  do something
})
btn.addEventListener('click',fn2)//具名函数

addEventListener()将事件加入到监听队列中,当浏览器发现用户点击按钮时,click 队列中依次执行匿名函数1、具名函数2。

通过addEventListener()添加的事件只能由removeEventListener()来移除,并且removeEventListener()只能移除具名函数,不能移除匿名函数。

IE中DOM2级事件

IE8 及之前,实现类似addEventListener()和removeEventListener()的两个方法是attachEvent()和detachEvent(),这两个方法接受相同的两个参数。

  1. 要处理的事件名;
  2. 作为事件处理程序的函数;
    IE8 之前的只支持事件冒泡,所以通过attachEvent()添加的事件处理程序只能添加到冒泡阶段。
btn.attachEvent('click',fn1)
btn.attachEvent('click',fn2)

当浏览器发现用户点击按钮时,click 队列中依次执行fn1,fn2
类似的detachEvent()也只能移除具名函数,不能移除匿名函数。

兼容处理
 if(typeof btn.addEventListener === 'function'){
  btn.addEventListener('click',fn);
}else if(typeof btn.attachEvent === 'function'){
  btn.attachEvent('onclick',fn)
}else{
  btn.onclick=function(){
    // do something
  }
}

事件捕获&事件冒泡

通常,一个事件会从父元素开始向目标元素传播,然后它将被传播回父元素。

DOM事件规定的事件流包括三个阶段:

  • 事件捕获阶段;事件从父元素开始向目标元素传播,从 Window 对象开始传播。
  • 处于目标阶段;该事件到达目标元素或开始该事件的元素。
  • 事件冒泡阶段;这时与捕获阶段相反,事件向父元素传播,直到 Window 对象。
    在这里插入图片描述
    1. 事件捕获
.addEventListener("click", () => {
    console.log('div');
  },true); //第三个参数true,为事件捕获时触发

最不具体的节点最先收到事件,而最具体的节点最后收到事件。事件捕获实际上是为了在事件到达最终目标前拦截事件。
点击<div>元素会以下列顺序触发 click 事件:

- document; 
- <html>;
- <body>;
- <div>;

2. 事件冒泡

.addEventListener("click", () => {
    console.log('div');
  },true); //第三个参数false 。表示在冒泡阶段调用事件,默认为 false

在点击页面中的 <div> 元素后,click 事件会以如下顺序发生:

-  <div>;
-  <body>;
-  <html>;
- document; 

最先触发 click 事件。然后,click 事件沿 DOM 树一路向上,在经过的每个节点上依次触发,直至到达 document 对象。

事件捕获和事件冒泡属于两个相反的过程
实例:

 
<div id="testDiv">
      testDiv
      <ul id="testUl">
        testUl
        <li id="testIi">
          testIi
        </li>
      </ul>  
   </div>

     var testDiv=document.getElementById('testDiv');
       var testUl=document.getElementById('testUl');
       var testIi=document.getElementById('testIi');

      window.addEventListener("click", () => {
          console.log('Window click');
        },true);//事件捕获时执行

      document.addEventListener("click", () => {
          console.log('Document click');
        }); // 事件冒泡时执行
      
        testDiv.addEventListener("click", () => { 
          console.log('DIV click');
        }); // 事件冒泡时执行

        testUl.addEventListener("click", () => {
          console.log('UL click');
        },true);//事件捕获时执行

        testIi.addEventListener("click", () => {
          console.log('LI click');
        },true);//事件捕获时执行

点击testIi时,先进行事件捕获,从window开始到testIi节点本身,依次顺序是window、document、testDiv、testUl、testIi,其中window、testUl注册了事件捕获,所以打印“Window click”、“UL click”。然后执行目标阶段“LI click”。最后进行事件冒泡依次顺序是testIi、testUl、testDiv、document、window,其中testDiv、document、注册了事件冒泡,所以执行“DIV click”、”Document click“
在这里插入图片描述
可以同时注册事件捕获和事件冒泡吗?
答案是可以

  var testDiv=document.getElementById('testDiv');
       var testUl=document.getElementById('testUl');
       var testIi=document.getElementById('testIi');

      window.addEventListener("click", () => {
          console.log('Window click');
        },true);//事件捕获时执行

      document.addEventListener("click", () => {
          console.log('Document click');
        }); // 事件冒泡时执行
      
        testDiv.addEventListener("click", () => { 
          console.log('DIV click');
        }); // 事件冒泡时执行

        testUl.addEventListener("click", () => {
          console.log('捕获UL click');
        },true);//事件捕获时执行
        testUl.addEventListener("click", () => {
          console.log('冒泡UL click');
        },false);//事件捕获时执行
        testIi.addEventListener("click", () => {
          console.log('LI click');
        },true);//事件捕获时执行

点击testIi的执行结果
在这里插入图片描述

点击testUl执行结果
在这里插入图片描述

事件对象

event对象

DOM0和DOM2的事件处理程序都会自动传入event对象
IE中会有window.event、event两种情况,(只有在事件处理程序执行期间,event对象才会存在;一旦事件处理程序执行完成,event对象就会被销毁)

event对象里需要关心的属性:

  1. target:target永远是被触发事件的那个元素;
  2. currentTarget:被绑定事件的元素
  3. eventPhase:调用事件处理程序的阶段,有三个值
    a. 捕获阶段;
    b. 处于目标;
    c. 冒泡阶段;

所以获取event对象兼容写法

// 获取event对象
  getEvent: (event) => {
    return event ? event : window.event
  },

event.currentTarget和event.target通常情况下相同,不过如果当前事件是在冒泡或者捕获阶段被调用,则两者的值不同,target的值为触发事件的DOM,currentTarget的值为绑定事件的DOM,我们可以借助target属性实现事件委托,又称事件代理(下面介绍)。
父级套多个子级,父级上绑定事件,点击子级,通过冒泡触发父级的绑定事件,此时event.currentTarget就能拿到父级事件的节点,event.target拿到对应触发的子级

preventDefault和stopPropagation

stopPropagation:阻止事件传播。但是默认事件任然会执行,当你调用这个方法的时候,如果点击一个连接,这个连接仍然会被打开。
preventDefault:取消事件的默认动作,但是冒泡仍然会发生。

(JQ里 return false等效于同时调用e.preventDefault()和e.stopPropagation())

stopPropagation:遇到stopPropagation停止捕获/目标/冒泡
如果没有stopPropagation使用以上面例子来说执行顺序是:window捕获->document捕获->testDiv捕获->testUl捕获->testIi捕获->testIi冒泡->testUl冒泡->testDiv冒泡->document冒泡->window冒泡

       var testDiv=document.getElementById('testDiv');
       var testUl=document.getElementById('testUl');
       var testIi=document.getElementById('testIi');
  
      window.addEventListener("click", () => {
          console.log('捕获Window click');
        
        },true);//事件捕获时执行

      document.addEventListener("click", () => {
          console.log('冒泡Document click');
        }); // 事件冒泡时执行
      
        testDiv.addEventListener("click", () => { 
          console.log('冒泡DIV click');
        }); // 事件冒泡时执行
        testUl.addEventListener("click", () => {
          console.log('冒泡UL click');
          
        },false);//事件捕获时执行
        testUl.addEventListener("click", (event) => {
          console.log('捕获UL click');
          event.stopPropagation() //依次打印  捕获Window click、捕获捕获UL click
        },true);//事件捕获时执行
        
        testIi.addEventListener("click", (event) => {
          console.log('捕获LI click');

        },true);//事件捕获时执行
        testIi.addEventListener("click", (event) => {
          console.log('冒泡LI click');

        },false);//事件冒泡时执行

在 testUl捕获事件上使用 event.stopPropagation(),事件执行到testUl捕获,不会往下执行testIi捕获->testIi冒泡->testUl冒泡->testDiv冒泡->document冒泡->window冒泡,阻止事件向后传播
在这里插入图片描述
在 testIi冒泡事件上使用 event.stopPropagation(),事件执行到testIi冒泡,不会往下执行estUl冒泡->testDiv冒泡->document冒泡->window冒泡,阻止事件向后传播

  testIi.addEventListener("click", (event) => {
          console.log('冒泡LI click');
            event.stopPropagation() //依次打印  捕获Window click、捕获捕获UL click 捕获LI click  冒泡LI click
        },false);//事件冒泡时执行

在这里插入图片描述
preventDefault:取消事件的默认动作,但是冒泡仍然会发生。
点击Checkbox时,复选框会默认选中
加上event.preventDefault(),点击不会默认选中,但是冒泡仍然会发生(捕获也是会发生的)

<div id="testDiv">
      testDiv
      <ul id="testUl">
        testUl
        <li id="testIi">
          testIi
        </li>
        <li id="testIi2">
          <form>
            <label for="id-checkbox">Checkbox:</label>
            <input type="checkbox" id="id-checkbox" />
          </form>
        </li>
      </ul>  
    </div>
  var testDiv=document.getElementById('testDiv');
       var testUl=document.getElementById('testUl');
       var testIi=document.getElementById('testIi');
       var testIi2=document.getElementById('testIi2');
      window.addEventListener("click", () => {
          console.log('捕获Window click');
        
        },true);//事件捕获时执行

      document.addEventListener("click", () => {
          console.log('冒泡Document click');
        }); // 事件冒泡时执行
      
        testDiv.addEventListener("click", () => { 
          console.log('冒泡DIV click');
        }); // 事件冒泡时执行
        testUl.addEventListener("click", () => {
          console.log('冒泡UL click');
          
        },false);//事件捕获时执行
        testUl.addEventListener("click", () => {
          console.log('捕获UL click');
          
        },true);//事件捕获时执行
        
        testIi.addEventListener("click", (event) => {
          console.log('捕获LI click');

        },true);//事件捕获时执行
        testIi.addEventListener("click", (event) => {
          console.log('冒泡LI click');
        },false);//事件冒泡时执行
         testIi2.addEventListener("click", (event) => {
         console.log('Checkbox click');
          // event.stopPropagation()
          event.preventDefault()
        },true);//事件捕获时执行

点击checkbox后
在这里插入图片描述

IE8及以下不支持preventDefault和stopPropagation()

IE中对应的属性:

  • srcElement => target
  • returnValue => preventDefaukt()
  • cancelBubble => stopPropagation()

所以兼容写法

  // 获取当前目标
  getTarget: (event) => {
    return event.target ? event.target : event.srcElement
  },
  // 阻止默认行为
  preventDefault: (event) => {
    if (event.preventDefault) {
      event.preventDefault()
    } else {
      event.returnValue = false
    }
  },
  // 停止传播事件
  stopPropagation: (event) => {
    if (event,stopPropagation) {
      event.stopPropagation()
    } else {
      event.cancelBubble = true
    }
  }

事件委托

事件委托:用来解决事件处理程序过多的问题,找到共同父级,在父级绑定事件,子级触发事件,父级事件里进行判断,走不同的分支

页面结构如下

<ul id="myLinks">
  <li id="goSomewhere">Go somewhere</li>
  <li id="doSomething">Do something</li>
  <li id="sayHi">Say hi</li>
</ul>

按照传统的做法,需要像下面这样为它们添加 3 个事 件处理程序。

var item1 = document.getElementById("goSomewhere");
var item2 = document.getElementById("doSomething");
var item3 = document.getElementById("sayHi");
EventUtil.addHandler(item1, "click", function(event){
    location.href = "http://www.xianzao.com";
});
EventUtil.addHandler(item2, "click", function(event){
    document.title = "I changed the document's title";
});
EventUtil.addHandler(item3, "click", function(event){
    alert("hi");
});

如果在一个复杂的 Web 应用程序中,对所有可单击的元素都采用这种方式,那么结果就会有数不 清的代码用于添加事件处理程序。此时,可以利用事件委托技术解决这个问题。使用事件委托,只需在 DOM 树中尽量最高的层次上添加一个事件处理程序,如下面的例子所示

var list = document.getElementById("myLinks");
EventUtil.addHandler(list, "click", function(event) {
  event = EventUtil.getEvent(event);
  var target = EventUtil.getTarget(event);
  switch(target.id) {
  case "doSomething":
      document.title = "I changed the document's title";
      break;
  case "goSomewhere":
      location.href = "http://www.wrox.com";
      break;
  case "sayHi": 9 alert("hi");
    break; 
  }
}

子节点的点击事件会冒泡到父节点,并被这个注册事件处理

最适合采用事件委托技术的事件包括 click、mousedown、mouseup、keydown、keyup 和 keypress。 虽然
mouseover 和 mouseout 事件也冒泡,但要适当处理它们并不容易,而且经常需要计算元素的位置。

  • 19
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值