DOM事件模型与事件委托

1,176 阅读8分钟

一 、事件的几个基本定义

1.什么是事件?

浏览器赋予元素天生默认的一些行为,不论是否绑定相关的方法,只要行为操作进行了,那么一定会触发相关的事件行为。

2.什么是事件绑定?

给元素的某一个事件行为绑定方法,目的是行为触发会可以做点自己想做的事情。

3. DOM0和DOM2事件绑定

1.【DOM0事件绑定】

        元素.onxxx=function(){}
	元素.onxxx=null;
  1. 原理:给DOM元素对象的某一个私有事件属性赋值函数值,当用户触发这个事件行为,JS引擎会帮助我们把之前绑定的方法执行的
    • 不是所有的事件类型都支持这种方式,元素有哪些onxxx事件属性,才能给其绑定方法(例如:DOMContentLoaded事件就不支持这种绑定方案)
    • 只能给当前元素的某一个事件行为绑定一个方法(绑定多个也只能识别最后一个)

2.【 DOM2事件绑定】

    // 绑定
    元素.addEventListener([事件类型],[方法],[传播模式])
    // 移除
    元素.removeEventListener([事件类型],[方法],[传播模式])
    // 便于移除,把函数提到外面写
    function anonymous(){
                    console.log('ok');
            }
    box.addEventListener('click',anonymous,false);
    box.removeEventListener('click',anonymous,false);
  1. 原理:基于原型链查找机制,找到EventTarget.prototype上的addEventListener方法执行,它是基于浏览器事件池机制完成事件绑定的

事件池

addEventListeener:向事件池不断增加内容 cM1URf.png)

DOM0和DOM2都绑定了

不会相互影响,因为是2个不同的机制

4. 事件对象

  1. 当前元素的某个事件行为被触发,不仅会把绑定的方法执行,还会给绑定的方法传递一个实参,这个实参就是事件对象。
  2. 事件对象就是用来存储当前行为操作相关信息的对象;(MosueEvent/KeyboardEvent/Event/TouchEvent...)
  3. =>事件对象和在哪个方法中拿到的没关系,它记录的是当前操作的信息。

二、DOM事件模型与事件委托

DOM事件模型

先看一段代码

 <div class="爷爷">
        <div class="爸爸">
            <div class="儿子">文字</div>
        </div>
</div>

给3个div分别添加事件监听fnYe/fnBa/fnEr。有2个问题:

1. 提问1:点击了谁?

点击文字,算不算点击儿子?

点击文字,算不算点击爸爸?

点击文字,算不算点击爷爷?

答案:都算

2. 提问2:调用循序

点击文字,最先调用fnYe/fnBa/fnEr中的那一个函数?

答案:都行。IE5调用顺序为fnEr->fnBa->fnYe, 网景调用顺序为fnYe->fnBa->fnEr。

W3C在2002年发布了标准, 规定浏览器同时支持两种调用顺序.首先按爷爷->爸爸->儿子顺序看有没有函数监听, 然后按儿子->爸爸->爷爷顺序看有没有函数监听.

专业术语是DOM事件模型的事件捕获事件冒泡.一个事件发生后,会在子元素和父元素之间传播(propagation)。

事件传播机制

事件的传播机制: 捕获节点- 目标阶段 - 冒泡阶段

window-> document -> HTML ->body -> outer -> inner -> center

  • 当一个元素接收到事件的时候,会把他接收到的事件传给自己的父级,一直到window
  • 当然其传播的是事件,绑定的执行函数并不会传播,如果父级没有绑定事件函数,就算传递了事件,也不会有什么表现,但事件确实传递了
  1. 因此DOM事件模型分为3个阶段:
    (1)捕获阶段:事件从window对象自上而下向目标节点传播的阶段(示例代码中简化为: 爷爷->爸爸->儿子);

     从最外层容器一直向里层查找,直到到当前触发的事件源为止。查找的目的是建立起当前元素未来冒泡传播的路线。
    

(2)目标阶段:真正的目标节点正在处理事件的阶段;(示例代码中: 文字)

    把当前元素的相关事件触发(绑定方法,则把方法执行)

(3)冒泡阶段:事件从目标节点自下而上向window对象传播的阶段(示例代码中简化为: 儿子->爸爸->爷爷)。

     1. 不仅当前元素的相关事件行为被触发,而且再捕获阶段获取的传播路径中的每一个元素的相关事件都会被触发【从里到外】
     2. 也就是说,其父级的所有元素的相关事件也会被触发
     3. 如果也对应绑定了方法,方法也会被触发。

cSqhdS.png

捕获和冒泡

由外向内找监听函数, 叫事件捕获.

由内向外找监听函数, 叫事件冒泡.

绑定事件API

DOM0

绑定的方法都是在目标或者冒泡阶段执行.

DOM2(addEventListener事件)

可以控制在捕获阶段执行

IE5*:div1.attachEvent('onclick',fn)//冒泡

网景:div1.addEventListener('click',fn)//捕获

W3C:div1.addEventListener('click',fn,bool)

如果bool不传或为falsy

就让fn走冒泡,即当浏览器在冒泡阶段发现baba有fn监听函数,就会调用fn,并提供时间信息。

如果bool为true

就让fn走捕获,即当浏览器在捕获阶段发现baba有fn监听函数,就会调用fn,并且提供事件信息。 cSL9zR.png

        document.body.onclick =(ev)=>{
                console.log("body",ev)
        }
        outer.onclick =(ev)=>{
                console.log("outer",ev)
        }
        inner.onclick =(ev)=>{
                console.log("inner",ev)
        }
        center.onclick =(ev)=>{
                console.log("center",ev)
               // ev. stopPropagation() //  阻止冒泡,阻止之后再运行一下看看
        }

问:以上四个ev(事件对象)是不是同一个?
答:是同一个。事件对象和在哪个方法中没有关系,事件对象是用来记录当前操作的相关信息的,即操作一次记录一次,此时不论在哪里获取的ev,都是记录本次信息。
除非重新操作,那么上次信息就没有了,ev存储的是最新的操作信息。
  1. 当点击中间的center时,依次打印出center=>inner=>outer=>body
  2. 若阻止冒泡,只打印出center 演示代码链接

代码解释

代码

e.target 和e.currentTarget区别

  1. 区别:

e.target - 用户操作的元素
e.currentTarget-程序员监听的元素
this是e.currentTarget,我个人不推荐使用它

  1. 举例:

div>span{文字},用户点击文字
e.target就是span
e.currentTarget就是div

一个监听顺序的特例

  1. 背景

只有一个div被监听(不考虑父子同时被监听)
fn分别再捕获阶段和冒泡阶段监听click事件
用户点击的元素就是开发者监听的

  1. 代码
div.addEventListenter('click',f1)

div.addEventListenter('click',f2,true)

请问,f1先执行还是f2先执行?如果把两个调换位置?
3. 答案: 谁先监听谁先执行。

取消冒泡和阻止默认事件

1. 取消冒泡

捕获不可取消,但冒泡可以

  1. event.stopPropagation()可中断冒泡,浏览器不再向上走。
    【W3C标准event.stopPropagation();但不支持IE9以下版本】

【IE独有(谷歌也实现了)event.cancelBubble = true;】

function stopBubble(e) { 
        if(e && e.stopPropagation) { //非IE 
          e.stopPropagation(); 
        } else { //IE 
          window.event.cancelBubble = true; 
        } 
      } 
  1. 一般用于封装某些独立的组件
  2. Bubbles ——该事件是否冒泡,所有冒泡都可取消
  3. Cancelable ——是否支持开发者阻止默认事件
  • 不是所有的事件都能冒泡。以下事件不冒泡:blurfocusloadunload
  • 事件解决方案方式在不同浏览器,可能是有所区别的,有些不支持捕获型方案,多数浏览器默认冒泡型方案。
  • 阻止冒泡并不能阻止对象默认行为,例如submit按钮被点击后会提交表单数据,需使用e.preventDefault();阻止默认行为,IE则是window.event.returnValue = false;

2. 阻止默认事件

  1. 默认事件——表单提交,a标签跳转,右键菜单等
  2. 方法:
return false;以对象属性的方式注册的时间才生效
event.preventDefault()W3C标注,IE9以下不兼容
event.returnValue = false;兼容IE
/假定有链接<a href="http://baidu.com/" id="testA" >baidu.com</a>
var a = document.getElementById("testA");
a.onclick =function(e){
    if(e.preventDefault){
        e.preventDefault();
    }else{
        window.event.returnValue == false;
    }
}

三、事件委托

自定义事件

  1. 浏览器的自定义事件。MDN列表
  2. 自定义事件:例子

事件委托

“事件代理”即是把原本需要绑定在子元素的响应事件(click、keydown......)委托给父元素,让父元素担当事件监听的职务。事件代理的原理是DOM元素的事件冒泡

参考一个文章

应用场景

  1. 场景1:

要给100个按钮添加点击事件,咋办?

答:监听这个100个按钮的祖先,等冒泡的时候判断target是不是这100个按钮中的一个

代码

  1. 场景2:

你要监听目前不存在的元素的点击事件?

答:监听祖先,等点击的时候看看是不是监听的元素即可。

代码

优点

  1. 省监听数(内存):可以大量节省内存占用,减少事件注册,比如在ul上代理所有li的click事件就非常棒。
  2. 可以动态监听元素: 可以实现当新增子对象时无需再次对其绑定(动态绑定事件)

封装一个事件委托

  1. 要求:实现一个on('click','#testDiv','li',fn)函数,要求可以实现事件委托。当用户点击#testDiv里面的li元素时,调用fn函数。
  2. 代码