DOM事件

DOM事件流

由于ie和网景对事件发生顺序的异议,w3c制定了DOM2级事件的事件模型。
w3c规定了两种调用顺序,假设有三层div嵌套,分别有自己的监听函数,事件捕获(网景提出)为从外到内顺序监听(DOM树由下到上),事件冒泡(ie提出)为从内到外顺序监听。冒泡从window开始,到window结束,就算由iframe也是这样。
image.png

DOM事件的级别

DOM一共有四个级别,0、1、2、3,但是1级没有事件内容,因此DOM事件一共有3个级别。
0级就是通过属性来绑定元素的属性,例如div.onclick=fn(),赋值null可以解绑div.onclick=null
2级就是通过addEventListener来添加事件
3级就是在2级的基础上增加的很多事件,例如滚动、焦点、输入文本等等事件

两种事件顺序及触发方法

首先,浏览器一直会先进行捕获阶段,然后进行冒泡阶段,在两个阶段中对每一个元素寻找监听事件,不同的是事件的设置方式。
一开始ie和网景分了两种事件监听的函数,分别为attachEvent(冒泡)和addEventListener(捕获),w3c规定aaddEventListener为标准,只是用参数来设置发生的阶段,aaddEventListener接收3个参数,第一个为事件种类,第二个为事件内容,第三个为事件的阶段,默认为false为冒泡阶段,而true为捕获阶段,如下所示。

  1. div.addEventListener('mousedown',(e)=>{console.log(e)},false);

注意:addEventListener可以传给里面的事件函数一个参数e,e可以传入用户操作事件的相关信息,e有两个属性,e.target表示用户操作的属性,e.currentTarget表示程序员监听的元素,事件函数中的this表示e.currentTarget。
注意:在同一元素上同时设置冒泡与捕获事件,且事件是一样的,那么先进行的是冒泡的事件,然后进行捕获事件。

  1. div.addEventListener("click", (e) => console.log(1));
  2. div.addEventListener("click", (e) => console.log(2), true);
  3. //先打印1,后打印2

可以取消事件的冒泡阶段

捕获阶段是不可以取消的,但是冒泡可以取消,使用e.stopPropagation(),这样后续元素的冒泡阶段的事件会被取消,即浏览器到此为止不会向上查询了,但是同一元素的冒泡事件不会被取消。

  1. div.addEventListener("click", (e) => {e.stopPropagation();
  2. console.log(1)});

以上方法在ie11以下无效,需要使用e.cancelBubble = true。

event对象

上文的e其实就是event对象
type:

发生的事件的类型,例如”click”, “mouseover”

target:

发生事件的节点,可能与 currentTarget 不同

currentTarget:

正在处理事件的节点,如果在 capturing 阶段和bubbling阶段处理事件,这个属性就与 target 属性不同。在事件监听函数中应该用这个属性而不是 this.

stopPropagation():

可以阻止事件从当前正在处理他的节点传播

preventDefault():

阻止浏览器执行与事件相关的默认动作,与0级DOM中返回false一样

clientX, clientY:

鼠标相对于浏览器的x坐标y坐标

screenX, screenY:

鼠标相对于显示器左上角的x坐标y坐标

不支持取消冒泡的事件类型

例如如下的scroll滚动事件的冒泡是不能取消的。bubbles是指是否可以冒泡,cancelable指的是该事件是否可以取消冒泡。
image.png
注意:不可冒泡的事件如focus,blur,change,submit,reset,select等事件不冒泡。

那么如何阻止滚动事件呢?

scroll 事件既不能 stopPropagation 也不能 preventDefault,看上去是阻止了 scroll 事件, 其实是 preventDefault 滚轮/翻页按键事件。
另外,我们要知道,如果有一个很长很长的div需要滚动,其实滚动条不是存在在div上的,而是存在在document上的,因此设置div不可以滚动,但是页面其他地方还是可以滚动的。
这里我们来阻止一下鼠标的滚动事件。(另外还有键盘和移动端)
1.wheel事件的阻止
wheel事件和scroll事件是不同的,wheel是指滚轮的滚动,scroll事件是滚动条的行动。因此没有滚动条就不会触发scroll。

  1. div.addEventListener("wheel", (e) => {
  2. e.preventDefault();
  3. });

这样我们就阻止了wheel的事件,用户无法使用滚轮滚动div中的内容。
2.css隐藏滚动条(用户无法用鼠标拖动)

  1. div::-webkit-scrollbar {
  2. width: 0 (!important);//这个可选
  3. }

其他阻止滚动的方法

其实这种事件的处理方案有许多种,但是他们的应用场景和性能、优缺点都是不同的,用到的时候在考虑。
可参考别人的处理方式https://segmentfault.com/a/1190000012313337?_ea=2953311

阻止默认事件

常见默认事件:表单提交,a标签跳转,右键菜单…
阻止默认事件一般有以下三种方法
可以使用e.defaultPrevented查看是否被阻止了默认行为

//谷歌及IE8以上
e.preventDefault();
//IE8及以下
window.event.returnValue = false;
//无兼容问题(但不能用于节点直接onclick绑定函数),有几种特殊事件也不能这样取消,所以尽量避免
return false;
//可以封装一下,使其适配所有版本
 div.onclick = function ( e ){
    if(e && e.preventDefault) {
      //非IE浏览器
      e.preventDefault();
    } else {
      //IE浏览器(IE11以下)
      window.event.returnValue = false;
    }
    //return false;   //或者不写上面的判断直接写这句
  };

1.阻止链接跳转(return false的问题)

//return false的一个问题举例
<a href="https://www.baidu.com" id="a" onclick="fn()">123</a>
function fn(){return false};//这样可以执行其中的内容,但是对于取消点击是完全无效的
//并不是因为绑定方式的不同,因为 下面这样是无效的
<a href="https://www.baidu.com" id="a">123</a>
a.addEventListener("click", (e) => {return false;});
//是因为本身就默认有click事件,需要这样做
<a href="https://www.baidu.com" id="a">123</a>
a.addEventListener("click", (e) => {e.preventDefault();});

当然也可以选择替换a标签的地址(直接不填会刷新页面)

 <a href="javascript:;"></a>

2.阻止右键菜单

div.addEventListener("contextmenu", (e) =>{return false});//当然使用preventDefault()也是可以的
document.addEventListener("contextmenu", (e) =>{return false});//整个文档

事件种类

事件种类非常非常的多请参考mdn文档https://developer.mozilla.org/zh-CN/docs/Web/Events

自定义事件

开发者可以自定义事件
1.Event(代码借鉴网友)——Event对象的构造函数

<script type="text/javascript">  
 2     /* 创建一个事件对象,名字为newEvent,类型为build,可以修改事件的一些属性*/
 3     var newEvent = new Event('build', { bubbles:true,cancelable:true,composed:true });
 4         
 5     /* 给这个事件对象创建一个属性并赋值 */
 6     newEvent.name = "新的事件!";
 7     
 8     /* 将自定义事件绑定在document对象上,这里绑定的事件要和我们创建的事件类型相同,不然无法触发 */
 9     document.addEventListener("build",function(){
10         alert("你触发了自定义事件!" + newEvent.name);
11     },false)
12         
13     /* 触发自定义事件 */
14     document.dispatchEvent(newEvent);  
15 </script>

2.CustomEvent——CustomEvent对象的构造函数
CustomEvent的用法基本与Event一样,但是CustomEvent可以传递自定义的detail参数,当然也可以自定义属性。
具体可以参考https://zhuanlan.zhihu.com/p/108447200

事件委托

事件委托的使用场景与优点
1. 减少内存消耗
试想一下,若果我们有一个列表,列表之中有大量的列表项,我们需要在点击列表项的时候响应一个事件;

<ul id="list">
  <li>item 1</li>
  <li>item 2</li>
  <li>item 3</li>
  ......
  <li>item n</li>
</ul>
// ...... 代表中间还有未知数个 li

如果给每个列表项一一都绑定一个函数,那对于内存消耗是非常大的,效率上需要消耗很多性能;
因此,比较好的方法就是把这个点击事件绑定到他的父层,也就是 ul 上,然后在执行事件的时候再去匹配判断目标元素;
所以事件委托可以减少大量的内存消耗,节约效率。
2. 动态绑定事件
比如上述的例子中列表项就几个,我们给每个列表项都绑定了事件;
在很多时候,我们需要通过 AJAX 或者用户操作动态的增加或者去除列表项元素,那么在每一次改变的时候都需要重新给新增的元素绑定事件,给即将删去的元素解绑事件;
如果用了事件委托就没有这种麻烦了,因为事件是绑定在父层的,和目标元素的增减是没有关系的,执行到目标元素是在真正响应执行事件函数的过程中去匹配的;
所以使用事件在动态绑定事件的情况下是可以减少很多重复工作的。

哪些适合事件委托?

适合用事件委托的事件:click,mousedown,mouseup,keydown,keyup,keypress。
值得注意的是,mouseover和mouseout虽然也有事件冒泡,但是处理它们的时候需要特别的注意,因为需要经常计算它们的位置,处理起来不太容易。
不适合的就有很多了,举个例子,mousemove,每次都要计算它的位置,非常不好把控,在不如说focus,blur之类的,本身就没用冒泡的特性,自然就不能用事件委托了。

封装事件委托

通过向on函数中传入事件类型,委托对象,操作元素,事件函数,捕获与否几个参数,可以直接进行事件委托。

  //element是委托目标(css式样或html标签式样),selector是操作的元素(css选择器)
      function on(eventType, element, selector, fn, capture) {
        if (!(element instanceof Element)) {
          //element是给到的元素对象,判断给到的是css式样的还是html标签式样的
          //如果是css样式的要选取元素对象
          element = document.querySelector(element);
        }
        element.addEventListener(
          eventType,
          (e) => {
            const t = e.target;
            if (t.matches(selector)) {
              //检测实际操作的元素是否匹配传入的操作元素
              fn(e);
            }
          },
          capture
        );
      }