Skip to content
On this page

事件机制

  • 关键词
    • 事件捕获
    • 事件冒泡
    • 事件流

事件流

JS与HTML的交互是用事件实现的,事件流描述了页面接收事件的顺序。

事件冒泡

从具体的元素开始触发事件,向上传播。

所有的现代浏览器都支持

事件捕获

与事件冒泡相反,从上方的结点先接收事件,向下传播至具体的结点。

旧版的浏览器不支持

DOM事件流

DOM2 Events 规范规定事件流分为 3 个阶段: 事件捕获到达目标事件冒泡

事件捕获最先发生,为提前拦截事件提供了可能。然后,实际的目标元素接收到事件。最后一个阶段是冒泡,最迟要在这个阶段响应事件。

所有现在浏览器都支持 DOM 事件流,只有 IE8 以及更早的浏览器不支持

整个事件流中监听事件只能被触发一次?❎

只要没有显示的阻止( stopPropagation ) 事件传递,那么就会按照事件流传递。

事件处理程序

为了响应执行的某种动作( clickloadmouseover ... )而调用的 on 开头的函数被称为事件处理程序(事件监听器)。

HTML事件处理程序

html
<input type="button" value="click me" onclick="console.log('click')" />

浏览器会先创建一个函数来封装属性的值,这个函数有一个特殊的局部变量:event 用来保存 event 对象,函数中 this 指向事件的目标元素。

DOM0事件处理程序

js
const btn = document.getElementById("myBtn");
btn.onclick = function(){
  console.log('Clicked')
}

把一个函数赋值给 DOM 元素的事件处理程序的属性。兼容性最好,所有的浏览器都支持此方法。

这种事件处理是冒泡阶段的。

所赋值的函数被视为元素的方法,在元素的作用域中运行,this 指向该元素本身。可以通过 this 访问元素的任何属性和方法。

将事件处理程序属性设置为 null,即可移除该事件处理程序。

javascript
btn.onclick = null;

如果有多个 DOM0 事件处理程序的话,后面的是会把前面的给覆盖掉。只有执行最后一个调用的结果。

DOM2事件处理程序

在DOM 节点上通过 addEventListener()removeEventLinstener() 来添加和移除事件处理程序。

js
target.addEventListener(type, listener, options | useCapture);
  • type事件名
  • listener事件处理函数
  • options
    • capture表示 listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发
    • oncelistener调用一次后移除
    • passive设置为true时,表示 listener 永远不会调用 preventDefault()。如果listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。
  • useCapture指定函数执行是在事件流的捕获阶段还是冒泡阶段,默认false则为冒泡阶段,但对事件的传播是没有影响的。

DOM2 事件处理程序的一个优点是可以给一个元素添加多个事件处理程序,并按添加的顺序触发

使用addEventListener() 添加的事件处理程序只能使用 removeEventLinstener()移除(三个参数均一致才可以);所以,使用匿名函数添加的事件处理程序是不能被移除的。

Event 接口的 **preventDefault()**方法,告诉user agent:如果此事件没有被显式处理,它默认的动作也不应该照常执行。此事件还是继续传播,除非碰到事件侦听器调用stopPropagation()stopImmediatePropagation(),才停止传播。

IE事件处理程序

IE 实现事件处理程序的方法是: attachEvent()detachEvent()

这两个方法接收两个同样的参数:事件处理程序的名称( eg: onclick )和事件处理函数。

因为 IE8 及更早的版本只支持事件冒泡,所以使用 attachEvent() 添加的事件处理程序是添加在冒泡阶段。

注意:

  1. 作用域:attachEvent()是在全局作用域中运行的,所以 attachEvent() 中的函数中的 thiswindow
  2. 执行顺序:IE 事件处理程序的执行顺序是和添加顺序相反的。

事件对象

所有的浏览器都是支持这个 event 对象

DOM事件对象event

在事件处理函数内部,this 对象始终等于 currentTarget,因为 this 是指向调用的对象的;而target 是事件触发的实际目标,可能存在两者不相等的情况。

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="div"> Click me</div>
</body>
<script>
  var div = document.querySelector('#div');
  div.addEventListener('click', function(e){
    console.log('div click', e.currentTarget === this);    // true 
    console.log('div click', e.target === this);           // true
  })

  var body = document.querySelector('body');
  body.addEventListener('click', function(e){
    console.log('body click', e.currentTarget === this);   // true
    console.log('body click', e.target === this);          // false
  })
</script>
</html>

几个API

preventDefault()

event.preventDefault() 方法用于阻止事件的默认行为(比如,a 标签有跳转到 href 链接的默认行为,使用 preventDefault() 可以阻止这种导航行为)

preventDefault()阻止的必需是可 cancelable 的元素 **

Event 事件的 preventDefault 正常规范操作是先check是否可以 cancelableMDN check),如果 cancelable=false ,那就不能被阻止默认行为,这个代码就不能起作用。

参考MDN:大部分由用户与页面交互产生的原生浏览器事件都可以被取消。取消clickscrollbeforeunload 事件将分别阻止用户点击某些元素,滚动页面或跳离页面。)

stopPropagation()

event.stopPropagation() 方法用于立即阻止事件流在 DOM 中的传播,取消后续的事件捕获或冒泡。

scroll事件

  • 对于普通Element元素是不冒泡的
  • documentdefaultViewscroll事件冒泡,将body内放一个超出一屏的 div,然后监听 window 和 此 div 的 scroll 事件就可以

IE事件对象

  • 根据使用的事件处理程序不同而不同
    • 使用 DOM0 事件处理程序,event 对象是全局对象 window 的一个属性
    • 使用 attachEvent() / HTML 属性方法处理事件处理程序,event 对象会作为唯一的参数传给处理函数(event 仍然是 window 对象的属性,只是方便将其作为参数参入)

如何防止弹窗后底层元素滚动

当我们滚动鼠标滚轮,或者滑动手机屏幕时,触发对象可分为两种类型(详见W3C规范):

  1. viewport 被触发滚动, eventTarget 为关联的 Document
  2. element 元素被触发滚动,通常也就是我们添加 overflow 滚动属性的 element 元素, eventTarget 为相应的 node element

方案一:preventDefault 阻止弹出层默认的滚动事件

当我们触发滚轮或滑动时,如果当前元素没有设置 overflow 这样的属性,同时也没有 preventDefault 掉原生的滚动/滑动事件,那么此时触发的是 viewport 的滚动,position:fixed 的元素并没有什么例外。

那么如果不是事件冒泡,为什么会发生滚动穿透的问题呢?如果当前滚动的元素可以滚动,那么就会在当前元素触发 scroll 事件。如果当前的元素不能滚动(或滚动到边界不能再继续滚动),那么就会滚动外层元素。

因此,我们可以滚动到边界(上 下 左 右)的时候,如果继续滚动就preventDefault

html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    body {
      position: relative;
      width: 100vw;
      height: 10000px;
      margin: 0;
      background-image: linear-gradient(#e66465, #9198e5);
    }

    #mask {
      position: fixed;
      width: 100vw;
      height: 100vh;
      background-color: rgba(0, 0, 0, 0.6);
      top: 0;
      left: 0;
    }

    .modal {
      position: fixed;
      top: 0;
      left: 0;
      width: 100vw;
      height: 50vh;
      background: #fff;
      overflow: scroll;
    }
  </style>
</head>

<body>
  <div id="mask">
    <div class="modal">
      <h1>A</h1>
      <h1>B</h1>
      <h1>C</h1>
      <h1>D</h1>
      <h1>E</h1>
      <h1>F</h1>
      <h1>G</h1>
      <h1>H</h1>
      <h1>I</h1>
      <h1>G</h1>
      <h1>K</h1>
      <h1>L</h1>
      <h1>M</h1>
    </div>
  </div>
</body>
<script>
  var mask = document.querySelector('#mask');
  var modal = document.querySelector('.modal');

  // 记录初次touch纵坐标
  let startY = 0;

  const modalHeight = modal.clientHeight;
  const modalScrollHeight = modal.scrollHeight;

  modal.addEventListener("touchstart", (e) => {
    startY = e.touches[0].pageY;
  })

  mask.addEventListener("touchmove", (e) => {
    let endY = e.touches[0].pageY;
    let delta = endY - startY;

    
    // 滚动到边界(上 下)的时候,如果继续滚动就preventDefault()。
    if (
      (modal.scrollTop === 0 && delta > 0) ||
      (modal.scrollTop + modalHeight === modalScrollHeight &&
        delta < 0)
    ) {
      e.preventDefault()
    }
  }, true);
</script>

</html>

方案二:设置底层元素的 overflow: hidden

在弹窗弹出的时候,手动修改 body 的样式,然后记得在弹窗关闭的时候将 body 的样式更改回去。

事件代理

如果一个节点中的子节点是动态生成的,那么子节点需要注册事件的话应该注册在父节点上。

事件代理的方式相较于直接给目标注册事件来说,有以下优点:

  • 节省内存
  • 不需要给子节点注销事件
html
<ul id="ul">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
</ul>
<script>
	let ul = document.querySelector('#ul');
	ul.addEventListener('click', (event) => {
		// 注意是target
		console.log(event.target); 
	});
</script>

文章参考链接: 深入理解浏览器的事件机制

MIT Licensed | Copyright © 2021 - 2022