深度讲解DOM事件模型

1.1 DOM事件的基础知识

事件:是元素天生自带的默认行为
不论我们是否给其绑定方法,当我们操作的时候,也会把对应的事件触发
事件绑定:给元素的某个行为绑定一个方法
目的:当事件触发时,可以做一些事情
常用的事件行为
【鼠标事件】

click 点击 abclick 双击 mousedown 鼠标按下 mouseup 鼠标抬起 musemove 鼠标移动 mouseover 鼠标滑过 mouseout 鼠标滑出 mouseenter 鼠标进入 mouseleave 鼠标离开
mousewhell 鼠标滚轮滚

【键盘事件】

keydown 按下某个键 keyup 释放某个键 key press 除shift/fn/capslock键以外,其他键按住(连续触发)

【手指事件】

1.单手指事件模型 Touch touchstart 手指按下 touchmove 手机移动 touchend 手指松开 touchcancel 操作取消(一般应用于非正常状态下操作结束) 2.多手指事件模型 Gesture gesturestart gesturechange/gestureupdate gestureend gesturecancel

【表单常用事件】

focus 获取焦点 blur 失去焦点 change 内容改变

【音视频常用事件】

canplay 可以播放(资源尚未加载完成,播放中可能会卡顿) canplaythrough 可以播放(资源已加载完,播放中不会卡顿) play 开始播放 playing 播放中 pause 暂停播放

【其他常用事件】

load 资源以及相关资源加载完成 unload 资源卸载 beforeunload 当前页面关闭之前 error 资源加载失败 scroll 滚动事件 readstatechange AJAX请求状态事件 contextmenu 鼠标右键触发 …………

1.2 DOM0事件绑定 VS DOM2事件绑定

【DOM0】

  • 元素.on事件行为=function () {};
  • 事件绑定原理:给元素的私有属性赋值,当事件触发,浏览器会帮我们把赋的值执行,但是这样也导致“只能给当前元素某一个事件行为绑定一个方法”

    【DOM2】

  • 元素.addEventListener(事件行为,function () {} ,true/false); //=>IE6~8不兼容

  • 元素.attachEvent
  • 事件绑定原理:基于原型链查找机制,找到EventTarget.prototype上的方法并且执行,此方法执行,会把当前元素某个事件行为绑定的所有方法,存放到浏览器默认的事件池中(绑定几个方法,就会向事件池中存放几个),当事件行为触发,会把事件池中存储的对应的方法,一次按照顺序执行“给当前元素某一个事件行为绑定多个不同方法”
  • 基于addEventListener向事件池中增加方法,存在去重的机制,“同一个元素,同一个事件类型,在事件池中只能存储这一遍方法”
  • 事件绑定时,我们一般都采用实名函数,目的:这样可以基于实名函数移除事件绑定(从事件池中移除,所以需要指定好事件类型、方法等信息、(要和绑定的时候一样才可以移除))
  1. function fn1(){ console.log(1); }
  2. function fn2(){ console.log(2); }
  3. function fn3(){ console.log(3); }
  4. box.addEventListener('click', fn2, false);
  5. box.addEventListener('click', fn3, false);
  6. box.addEventListener('click', fn1, false);
  7. box.addEventListener('click', fn1, false);
  8. box.addEventListener('mouseover', fn1, false);
  9. //基于addEventListener向事件池增加方法,存在去重的机制 “同一个元素,同一个事件类型,在事件池中只能存储一遍这个方法,不能重复存储”

【移除绑定事件方法】

  • DOM0直接赋值为null即可: box.onclick = null;
  • DOM2从事件池中移除,所以需要指定好事件类型、方法等信息(要和绑定的时候一样才可以移除)

    • box.removeEventListener(‘click’, fn, false);

      【同一个事件绑定方法数量】

  • 给当前元素某一个事件行为绑定多个不同方法

  • 同一个元素,同一个事件类型,在事件池中只能存储这一遍方法

    【触发先后顺序】

  • 可以混在一起用:执行的顺序以绑定的顺序为主

    【支持事件类型】

  • DOM0中能做的事件绑定行为,DOM2都支持

  • DOM2里面一些事件,DOM0不一定能处理绑定,例如:transitionend、DOMContentloaded…..

    【传播机制】

  • DON0绑定的方法,只能在目标阶段和冒泡阶段触发执行

  • DOM2绑定的方法,我们可以控制在捕获阶段

    经典面试题:**window.onload VS $(document).ready()?**

    1. 这个题我知道,我之前看过部分JQ源码(根据个人情况慎重抛出,如果自己不懂会更尴尬)
    2. 1.$(document).ready() 采用的是DOM2事件绑定,监听的是DOMContentLoaded这个事件,所以只要DOM结构加载完成就会被触发执行,而且同一个页面中可以使用多次(绑定不同的方法,因为基于DOM2事件池绑定机制完成的)
    3. 2.window.onload必须等待所有资源都加载完成才会被触发执行,采用DOM0事件绑定,同一个页面只能绑定一次(一个方法),想绑定多个也需要改为window.addEventListener('load', function () {})DOM2绑定方式

2. 事件对象和事件传播机制

【事件对象】

定义:给元素的事件行为绑定方法,当事件行为触发方法会被执行,不仅被执行,而且还会把当前操作的相关信息传递给这个函数 => “事件对象”.

如果是鼠标操作,获取的是MouseEvent类的实例 =>鼠标事件对象
**

  • 鼠标事件对象->MouseEvent.prototype ->UIEvent.prototype ->Event.prototype ->Object.prototype
  • 键盘事件对象->
  • 除了以上:普通事件对象(Event)、还有手指(TocuchEvent)等

事件对象和函数以及给谁绑定的事件没啥必然关系,它存储的是当前本次操作的相关信息,操作一次只能有一份信息,所以在哪个方法中获取的信息都是一样的;第二次操作,存储的信息会把上一次操作存储的信息替换掉…;

每一次事件触发,浏览器都会这样处理一下

1.捕获到当前操作的行为(把操作信息获取到),通过创建MouseEvent等类的实例,得到事件对象EV
2.通知所有绑定的方法(符合执行条件的)开始执行,并且把EV当做实参传递给每个方法,所以在每个方法中得到的事件对象其实是一个
……
3.后面再重新触发这个事件行为,会重新获取本次操作的信息,用新的信息替换老的信息,然后继续之前的步骤…

【鼠标事件对象属性】

clientx/cliendy:当前鼠标触发点距离当前窗口左上角的x/y轴坐标 pageX/pageY**:当前鼠标出发点距离当前页面左上角的x/y轴坐标 type:事件触发的类型 traget:**事件源(操作的是哪个元素,那个元素就是事件源)

  • 不兼容的跨浏览器中可以使用scrElement属性获取,也代表的是事件源

preventDefault():用来阻止默认行为的方法

  • 不兼容的浏览器可以用:ev.returnValue=false 也可阻止默认行为

stopPropagation();阻止冒泡传播

  • 不兼容的浏览器可以使用:ev.cancelBubble=true也可以实现阻止默认行为

【键盘事件对象属性】

code & key:存储的都是按键,code更细致 keyCode & which: 存储的是键盘按键对应的码值

  • 方向键:37 38 39 40 =>左上右下
  • 空格SPACE 32
  • 空格SPACE 32
  • 回车ENTER 13
  • 回退BACK 8
  • 删除DEL 46
  • SHIFT 16
  • CTRL 17
  • ALT 18

    ……

【事件的传播机制】

捕获阶段:从最外层向最里层事件源依次查找(目的:是为了冒泡阶段实现计算好传播层的路径)
目标阶段:当前元素相关事件行为触发
冒泡传播:触发当前元素的某一个事件行为,不仅它的这个行为被触发了,而且它所有的祖先元素(一直到window)相关的事件行为都会被依次触发(从内到外的顺序) =>BUBBLING_PHASE:3 (Event.prototype)

  1. document.body.onclick = function (ev) {
  2. console.log('BODY', ev);
  3. }
  4. outer.onclick = function (ev) {
  5. console.log('OUTER', ev);
  6. // ev.stopPropagation();
  7. }
  8. inner.onclick = function (ev) {
  9. console.log('INNER', ev);
  10. }
  11. outer.addEventListener('click', function (ev) {
  12. console.log('OUTER', ev);
  13. }, true);
  14. inner.addEventListener('click', function (ev) {
  15. console.log('INNER', ev);
  16. }, false);
  17. center.onclick = function (ev) {
  18. console.log('CENTER', ev);
  19. //=>阻止冒泡传播
  20. ev.stopPropagation();
  21. } ;

【图解事件传播】

事件传播.png

3.深度理解mouseeneter && mouseover

【mouseover】

  • 本身不是进入,而是看鼠标在谁的上面,从子元素进入父元素,触法父元素的mouseover,从父元素进入子元素触发元素的mouseout…..
  • 需要冒泡排序去做一些事情的,需要用mouseover

【mouseenter】

  • 默认阻止了事件的冒泡传播
  • 盒子中有后代元素的,我尽可能用mouseenter

【图解mouseover&&mouseenter的区别】

mouseover和mouseenter的区别.png

2 事件委托&拖拽事件

2.1事件委托

1.基于事件的冒泡传播机制完成。 2.如果一个容器中很多元素都要在触发某一事件的时候做一些事情(原始方案:给元素每一个都单独绑定),我们只需要给当前元素这个事件行为绑定方法,这样无论是触发哪一个后代元素相关事件。由于冒泡传播机制,当前容器绑定的方法也都要被触发行为。 3.想知道点击的是谁(根据是谁做不同的事情),只需要基于事件对中的ev.target事件源获取即可。

//=>事件委托处理的优势 1.基于事件委托实现,整体性能要比一个个的绑定方案方法高出50%左右。 2.如果多元素触发,业务逻辑属于一体的,基于事件委托来处理更加好。 3.某些事件业务场景只能基于事件委托处理。

//=>在真实项目中,如果要操作的元素是基于js动态绑定的,那么“相关事件行为触发做些事情”的处理操作,我们尽可能基于事件委托来处理(事件委托可以给动态元素绑定事件)