Skip to content

关于事件的一些概念

  1. 事件源: 事件发生在哪个元素节点对象上, 谁就是这个事件的事件源
  2. 事件处理函数: 元素节点本身就是有事件的, 绑定事件: 是指添加事件处理函数, 当这个事情发生了之后, 应该做什么?
  3. 交互行为: 触发事件(比如点击) + 事件的反馈

绑定事件

绑定事件,绑定事件处理函数, 监听事件其实说的都是一回事, 就是当发生一个事件的时候, 程序如何处理? 比如: 当我点击按钮的时候, 应该去 "下载文件", 那么实现下载文件的这个功能的就是按钮点击事件的处理函数

  1. 句柄绑定事件(包括标签行内的绑定事件:内联/行内事件监听器)
    1. 同一个的事件只能绑定一次, 绑定多次会覆盖
    2. js 代码绑定的事件比行内绑定的方式优先级高
    3. 可以直接把代码写到标签上
    4. 只能监听 冒泡阶段,无法监听 捕获阶段
  2. addEventListener 方法绑定事件(推荐 👍)
    1. IE9 以下不支持这种方式绑定
    2. 同一个类型的事件,可以绑定多个事件处理函数
    3. w3c DOM 规范的注册/绑定事件的方式
    4. 既可以监听 冒泡阶段又可以监听 捕获阶段, 具体看第三个参数 mdn 文档
  3. attchEvent 方法绑定:
    1. IE8 及以下低版本的浏览器支持, chrom 是没有的
html
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>js</title>
    <style type="text/css">
      #app {
        background: black;
        width: 100px;
        height: 100px;
      }
    </style>
  </head>
  <body>
    <div id="app" onclick="test" onmouseenter="console.log(1)"></div>

    <!-- javascript -->
    <script>
      function test() {
        console.log(1);
      }

      var app = document.getElementById("app");
      app.onclick = function () {
        console.info("click handler"); // 执行了这个, 证明js绑定的事件优先级高于内联事件监听器, 而且只能绑定一次
      };
    </script>
  </body>
</html>

兼容低版本浏览器做法:

兼容低版本浏览器做法: 这里使用了惰性函数, 可以参考:

javascript
/**
 * 兼容: 添加事件处理函数
 * @param {HTMLElement} el 事件源
 * @param {String} type 事件类型
 * @param {Function} handler 事件处理函数
 * @returns
 */
function addEvent(el, type, handler) {
  if (el.addEventListener) {
    addEvent = function (el, type, handler) {
      el.addEventListener(type, handler, false);
    };
  } else if (el.attachEvent) {
    addEvent = function (el, type, handler) {
      el.attachEvent("on" + type, function () {
        handler.call(el);
      });
    };
  } else {
    addEvent = function (el, type, handler) {
      el["on" + type] = handler;
    };
  }
  return addEvent(el, type, handler); // 只有这一次执行会判断
}

/**
 * 兼容: 移除事件处理函数
 * @param {HTMLElement} el 事件源
 * @param {String} type 事件类型
 * @returns
 */
function removeEvent(el, type, handler) {
  if (el.removeEventListener) {
    removeEvent = function (el, type, handler) {
      el.removeEventListener(type, handler);
    };
  } else if (el.detachEvent) {
    removeEvent = function (el, type, handler) {
      el.detachEvent("on" + type, handler);
    };
  } else {
    removeEvent = function (el, type, handler) {
      el["on" + type] = null;
    };
  }
  return removeEvent(el, type, handler); // 只有这一次执行会判断
}

面试题:

闭包问题: 复习闭包那一节的笔记 https://www.yuque.com/liaohui5/js-base/fepwdu Q: 以下代码, 控制台会输出什么? 为什么? A: 输出 5 个 A, 运维绑定事件处理函数是, 这个函数并没有执行, 所以最后执行的时候, 获取 i 的值是 for 循环执行玩之后的值

Q: 如果想让它输出对应序号应该怎么做?? A: 如果是 ES6, 直接把 var i = 0 修改为 let i = 0 就可以, 因为 let 关键字没有变量提升的问题 如果是 ES3, 在循环的时候用 闭包 去保存 i 变量的值, 然后再绑定事件就没有问题了

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>js</title>
  </head>
  <body>
    <ul>
      <li>1</li>
      <li>2</li>
      <li>3</li>
      <li>4</li>
      <li>5</li>
    </ul>

    <!-- javascript -->
    <script>
      var lis = document.getElementsByTagName("li");
      var item;
      for (var i = 0, len = lis.length; i < len; i++) {
        item = lis[i];
        item.addEventListener(
          "click",
          function () {
            console.info(i);
          },
          false
        );

        // 如果想拿到对应 i 的值:
        // (function(i) {
        //  item = lis[i];
        //  item.addEventListener('click', function() {
        //    console.info(i);
        //  }, false);
        // })(i);
      }
    </script>
  </body>
</html>

移除事件处理函数

  1. removeEventListener 方法
  2. detachEvent ,对应 attachEvent 方法
  3. el.onclick = null 句柄绑定事件的方式重新赋值
html
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>js</title>
    <style>
      #app {
        width: 100px;
        height: 100px;
        background: #000;
      }
    </style>
  </head>
  <body>
    <div id="app"></div>

    <!-- javascript -->
    <script>
      var app = document.getElementById("app");

      // 点击事件处理函数
      var clickHandler = function () {
        // 只会输出一次, 因为输出以后, 这个事件就被移除了
        console.info("clicked");
        // 点击事件处理函数
        this.removeEventListener("click", clickHandler);
      };

      // 鼠标划入事件处理函数
      var mouseEnterHandler = function () {
        console.info("move enter");
        // 移除鼠标划入事件处理函数
        this.onmouseenter = null;
      };

      app.addEventListener("click", clickHandler, false);
      app.onmouseenter = mouseEnterHandler;
    </script>
  </body>
</html>

事件捕获现象 & 事件冒泡现象

W3C 官方文档

123.png

事件冒泡 event bubbling

当一个事件发生在一个元素上,它会首先运行在该元素上的处理程序, 然后运行其父元素上的处理程序,然后一直向上到其他祖先上的处理程序。

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>js</title>
    <style>
      #wrapper {
        width: 300px;
        height: 300px;
        background: red;
      }
      #outer {
        width: 200px;
        height: 200px;
        background: green;
        margin-left: 300px;
      }
      #inner {
        width: 100px;
        height: 100px;
        background-color: blue;
        margin-left: 200px;
      }
    </style>
  </head>
  <body>
    <div id="wrapper">
      <div id="outer">
        <div id="inner"></div>
      </div>
    </div>

    <!-- javascript -->
    <script>
      var wrapper = document.getElementById("wrapper");
      var outer = document.getElementById("outer");
      var inner = document.getElementById("inner");
      var useCapture = false;

      wrapper.addEventListener(
        "click",
        function () {
          console.info("wrapper");
        },
        useCapture
      );

      outer.addEventListener(
        "click",
        function () {
          console.info("outer");
        },
        useCapture
      );

      inner.addEventListener(
        "click",
        function () {
          console.info("inner");
        },
        useCapture
      );
    </script>
  </body>
</html>

Sep-14-2021 21-52-11.gif

line-box.gif

事件捕获 event capturing

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>js</title>
    <style>
      #wrapper {
        width: 300px;
        height: 300px;
        background: red;
      }
      #outer {
        width: 200px;
        height: 200px;
        background: green;
        margin-left: 300px;
      }
      #inner {
        width: 100px;
        height: 100px;
        background-color: blue;
        margin-left: 200px;
      }
    </style>
  </head>
  <body>
    <div id="wrapper">
      <div id="outer">
        <div id="inner"></div>
      </div>
    </div>

    <!-- javascript -->
    <script>
      var wrapper = document.getElementById("wrapper");
      var outer = document.getElementById("outer");
      var inner = document.getElementById("inner");
      var useCapture = true;

      wrapper.addEventListener(
        "click",
        function () {
          console.info("wrapper");
        },
        useCapture
      );

      outer.addEventListener(
        "click",
        function () {
          console.info("outer");
        },
        useCapture
      );

      inner.addEventListener(
        "click",
        function () {
          console.info("inner");
        },
        useCapture
      );
    </script>
  </body>
</html>

capture.gif

addEventListener 的第三个参数的作用

MDN 文档: addEventListener(type, handler, useCapture || options)

  1. type: 字符串, 绑定的事件类型
  2. handler: 函数, 事件处理函数
  3. useCpature: 布尔值, 指定处理函数实在哪个阶段执行 true: 事件捕获 false: 事件冒泡
  4. options: 对象
    1. options.captcha: 默认 false, 是否监听捕获阶段
    2. options.once: 默认 false, 执行一次监听函数后就移除监听函数
    3. options.passive: 默认 false, 但是部分浏览器给 window, document, document.body的某一些事件(比如: touchstart)做了性能优化, 将默值改为了 true, 当值为 true 的时候, 就无法调用, preventDefault()方法(会抛出异常), 也就是说 passive 为 true 就无法阻止默认行为
html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>测试 Event 对象的 isTrusted 属性</title>
    <style>
      #app {
        margin: 0;
        padding: 0;
        width: 100%;
        height: 1500px;
      }
    </style>
  </head>
  <body>
    <div id="app">可滚动内容</div>
    <script>
      var app = document.querySelector("#app");
      // app 的 touchstart 事件监听函数可以正常执行
      app.addEventListener("touchstart", function (e) {
        e.preventDefault();
        console.log("[app] e.defaultPrevented: ", e.defaultPrevented);
      });

      // document 的 touchstart 事件监听函会抛出异常:
      document.addEventListener("touchstart", function (e) {
        // [Intervention] Unable to preventDefault inside passive event listener due to target being treated as passive.
        // See https://www.chromestatus.com/feature/5093566007214080
        e.preventDefault();
        console.log("[app] e.defaultPrevented: ", e.defaultPrevented);
      });

      /***************************************
为什么 passive 参数设置为 true 能够挺高滚屏性能?

function handler(e) {
  // 1. touchstart 的默认行为是滚动
  // 2. 事件监听函数的执行顺序:
  //    2.1 先执行 handler
  //    2.2 然后执行默认行为
  console.log("handler-exec");
}

document.addEventListener('touchstart', handler, { passive: true });


passive 参数设置为 true 能够挺高滚屏性能的原因:
1. 阻止默认行为的方法不会调用( preventDefault() )
2. 用两线程出处理问题, 一个处理事件监听函数, 一个处理默认行为
***************************************/
    </script>
  </body>
</html>

捕获和冒泡的执行顺序

  1. 其他元素节点: 先捕获, 后冒泡
  2. 事件源元素节点: 谁先绑定, 谁先执行
  3. 事件捕获和冒泡的执行顺序都是按照 DOM 结构来触发的, 跟谁 js 监听代码在前, 谁在后没有关系

myflow.png

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>js</title>
    <style>
      #wrapper {
        width: 300px;
        height: 300px;
        background: red;
      }
      #outer {
        width: 200px;
        height: 200px;
        background: green;
        margin-left: 300px;
      }
      #inner {
        width: 100px;
        height: 100px;
        background-color: blue;
        margin-left: 200px;
      }
    </style>
  </head>
  <body>
    <div id="wrapper">
      wrapper
      <div id="outer">
        outer
        <div id="inner">inner</div>
      </div>
    </div>

    <!-- javascript -->
    <script>
      var wrapper = document.getElementById("wrapper");
      var outer = document.getElementById("outer");
      var inner = document.getElementById("inner");

      // ----------------------------- inner ------------------------------//
      inner.addEventListener(
        "click",
        function () {
          console.info("inner capture");
        },
        true
      );
      inner.addEventListener(
        "click",
        function () {
          console.info("inner bubble");
        },
        false
      );

      // ----------------------------- wrapper ------------------------------//
      wrapper.addEventListener(
        "click",
        function () {
          console.info("wrapper capture");
        },
        true
      );
      wrapper.addEventListener(
        "click",
        function () {
          console.info("wrapper bubble");
        },
        false
      );

      // ----------------------------- outer ------------------------------//
      outer.addEventListener(
        "click",
        function () {
          console.info("outer capture");
        },
        true
      );
      outer.addEventListener(
        "click",
        function () {
          console.info("outer bubble");
        },
        false
      );
    </script>
  </body>
</html>

useCpature.gif

取消冒泡/阻止浏览器默认事件

取消(停止)冒泡

有时候我们并不需要这样的冒泡, 所以就需要手动的去停止冒泡

javascript
/**
 * 取消冒泡
 * @param {Event} e
 * @returns
 */
function cancelBubble(e) {
  if (e.stopPropagation) {
    cancelBubble = function (e) {
      e = e || window.event;
      return e.stopPropagation();
    };
  } else {
    cancelBubble = function (e) {
      e = e || window.event; // window.event 兼容ie8及其以下版本的浏览器
      e.cancelBubble = true;
    };
  }

  return cancelBubble(e); // 只会判断这一次
}

阻止浏览器默认事件

浏览器默认有很多事件: 比如点击 a 标签就直接跳转了, 比如点击鼠标右键会出现一个菜单, 比如点击表单中的按钮会直接提交表单

javascript
// 取消浏览器默认事件
function preventDefault(e) {
  var e = e || widow.event;
  if (e.preventDefault) {
    // w3c standard
    preventDefault = function (e) {
      e.preventDefault(e);
    };
  } else {
    // ie: 9/8/7/6
    preventDefault = function (e) {
      e = e || widow.event;
      e.returnValue = false;
    };
  }
  return preventDefault(e);
}

不能冒泡的事件类型

事件说明
abort音/视频终止加载
resize调整浏览器窗口大小
error静态加载失败(比如图片)
load浏览器加载完网页(window.onload)
unload浏览器关闭网页时
mouseenter鼠标移入
mouseleave鼠标移出
blur失去焦点
focus获取焦点

Event 对象的其他属性&方法

target / currentTarget

target: 始终指向初次触发事件的 DOM 对象, 事件源 currentTarget: 指向当前触发事件的 DOM 对象

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>测试 Event 对象的 target/currentTarget 属性</title>
  </head>
  <body>
    <div id="wrapper">
      <button id="btn">button</button>
    </div>
    <script>
      function handler(e) {
        // 点击按钮:
        // target: 始终指向 #btn
        // currentTarget: 第一次指向  #btn, 然后冒泡, 第二次指向 #wrapper
        // target 可以做事件代理, 而 currentTarget 不行, 因为冒泡时, 他指向的 DOM 变了
        console.info("target:", e.target);
        console.info("currentTarget:", e.currentTarget);
      }
      document.querySelector("#wrapper").addEventListener("click", handler);
      document.querySelector("#btn").addEventListener("click", handler);
    </script>
  </body>
</html>

cancelable 事件是否可以通过阻止默认行为取消

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>测试 Event 对象的 cancelable 属性</title>
    <style>
      .scroll-wrapper {
        width: 100%;
        height: 1500px;
      }
    </style>
  </head>
  <body>
    <form action="" id="form">
      <input type="text" />
      <button type="submit">submit form</button>
    </form>
    <div class="scroll-wrapper">这是可以滚动内容</div>
    <script>
      function handler(event) {
        if (typeof event.cancelable !== "boolean" || event.cancelable) {
          // 如果当前被触发的事件可以取消就直接取消
          event.preventDefault();
          console.info(event.type + "事件被取消了", event);
        } else {
          // 如果不能取消
          console.warn(event.type + "事件不能取消", event);
        }
      }

      document.getElementById("form").addEventListener("submit", handler);
      document.addEventListener("wheel", handler);
    </script>
  </body>
</html>

bubbles 当前事件是否会向 DOM 树上层元素冒泡

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>测试 Event 对象的 bubbles 属性</title>
  </head>
  <body>
    <div id="div">
      <input type="text" id="ipt" />
      <button id="btn">button</button>
    </div>
    <script>
      function handler(e) {
        console.info(e);
        if (typeof e.bubbles === "boolean" && e.bubbles) {
          console.info(`${e.type} 事件会向上冒泡`, e);
        } else {
          console.warn(`${e.type} 事件不会向上冒泡`, e);
        }
      }

      function $(selector) {
        return document.querySelector(selector);
      }

      // focus 事件不会向上冒泡
      $("#ipt").addEventListener("focus", handler);

      // click 事件会向上冒泡
      $("#btn").addEventListener("click", handler);

      $("#div").addEventListener("click", function () {
        console.info("div 的click事件被触发了");
      });
    </script>
  </body>
</html>

defaultPrevented 判断当前事件是否调用了 preventDefault 方法

注意: 当 cancelbabelfalse或者 addEventListeneroptions.passivetrue时, preventDefault()是无法取消的

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>测试 Event 对象的 preventDefault 属性</title>
  </head>
  <body>
    <div id="outter">
      <button id="btn1">btn1</button>
      <button id="btn2">btn2</button>
    </div>

    <script>
      document.querySelector("#outter").addEventListener("click", function (e) {
        if (e.target.id === "btn1") {
          // 如果点击的是 #btn1 就执行阻止默认事件
          e.preventDefault();
        }
        console.info("是否调用了 preventDefault: ", e.defaultPrevented);
      });
    </script>
  </body>
</html>

eventPhase 当前事件流在哪个阶段

image.png

isTrusted: 判断事件是用户交互产生的, 还是 js 脚本手动创建的

true: 是用户操作产生的事件 false: js 脚本创建的事件

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>测试 Event 对象的 isTrusted 属性</title>
  </head>
  <body>
    <button id="btn">click me</button>

    <script>
      var btn = document.querySelector("#btn");
      btn.addEventListener("click", function (e) {
        console.info("isTrusted: ", e.isTrusted, e);
      });

      // 脚本创建的事件, 手动调用触发, 必须先添加监听函数, 才能触发监听函数
      const event = document.createEvent("MouseEvent");
      event.initEvent("click");
      btn.dispatchEvent(event);
    </script>
  </body>
</html>

stopPropagation() 阻止默认事件

只有事件的 cancelabletrue并且 addEventListeneroptions.passive为 false 的时候才可以阻止

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>测试 Event 对象的 stopProgation 方法</title>
  </head>
  <body>
    <form action="">
      <input type="text" id="ipt" />
      <button type="submit" id="submit-btn">submit form</button>
    </form>
    <script>
      function handler(e) {
        e.preventDefault();
        console.info("事件是否被阻止: ", e.defaultPrevented, e);
      }

      // input: focus 无法被阻止, 因为 cancelable 为 false
      document.querySelector("#ipt").addEventListener("focus", handler);

      // form: submit 无法被阻止, 因为 addEventListener 的 options.passive 为 true
      document.querySelector("#submit-btn").addEventListener("click", handler, {
        passive: true, // 如果是 false 就可以阻止表单提交, 因为 click 事件的 cancalable 为 true
      });
    </script>
  </body>
</html>

stopImmediatePropagation(): 阻止其他后续同类型的监听函数执行

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>测试 Event 对象的 type 属性</title>
    <style>
      .red {
        background: red;
      }
      .blue {
        background: blue;
      }
      .item {
        width: 100px;
        height: 100px;
      }
    </style>
  </head>
  <body>
    <div class="item red"></div>
    <div class="item blue"></div>
    <script>
      var red = document.querySelector(".red");
      var blue = document.querySelector(".blue");

      red.addEventListener("click", function (e) {
        console.info("red-click-1");
        // e.stopImmediatePropagation();
        // 如果在此处执行, 后面添加的 click 监听函数都不会执行
      });
      red.addEventListener("click", function (e) {
        console.info("red-click-2");
        e.stopImmediatePropagation();
        // 如果在此处执行, 后面添加的 click 监听函数都不会执行,
        // 但是前面添加的不受影响, 正常执行
      });
      red.addEventListener("click", function () {
        console.info("red-click-3");
      });

      // 不阻止, 会按照顺序添加监听函数并执行
      blue.addEventListener("click", function () {
        console.info("blue-click-1");
      });
      blue.addEventListener("click", function () {
        console.info("blue-click-2");
      });
      blue.addEventListener("click", function () {
        console.info("blue-click-3");
      });
    </script>
  </body>
</html>

Released under the MIT License.