jtwang7 / JavaScript-Note

JavaScript学习笔记
10 stars 2 forks source link

JS 事件篇 - 事件处理程序 #39

Open jtwang7 opened 3 years ago

jtwang7 commented 3 years ago

术语表示: 事件处理程序:响应事件所调用的回调函数。事件处理程序的名字以 "on" 开头,如 "onclick","onmousemove" 等 事件类型:响应事件的类型,如 "click","mousemove" 等

HTML 事件处理程序

HTML 事件处理程序支持在 HTML 标签的特定元素中,使用事件处理程序的名字以 HTML 属性的形式来指定响应事件的调用函数。

<input type="button" value="Click Me" onclick="console.log('Clicked')" />

在 HTML 属性中定义的事件处理程序可以包含精确的动作指令(如上),也可以调用页面其他地方定义的脚本:

<script>
  function showMessage() {
    console.log("Hello world!")
  }
</script>
<input type="button" value="Click Me" onclick="showMessage()" />
  1. 作为事件处理程序的函数可以访问全局作用域中的一切变量
  2. 事件处理程序会被传入一个 event 对象
  3. this 值被指向所在的目标元素

HTML 事件处理程序的缺点

  1. 时机问题:事件处理程序定义在 HTML 标签属性上,很可能在 HTML 标签元素已经完成渲染时,事件处理程序的回调函数还未完成定义,导致用户与元素交互时,无法找到对应的回调函数执行代码而报错。

    解决方法:将 HTML 事件处理程序封装在 try/catch 块中,在上述情况发生时静默失败。

    <input type="button" value="Click Me" onclick="try{showMessage();} catch(err){...}" />
  2. 不同浏览器对事件处理程序作用域链的扩展的处理结果可能不同。不同 JavaScript 引擎中标识符解析的规则存在差异,因此访问无限定的对象成员可能导致错误。

  3. HTML 与 JavaScript 强耦合:JavaScript 代码脚本与 HTML 标签属性强绑定,如果修改事件处理程序,必须在 HTML 和 JavaScript 两个地方修改代码。这也是开发者不使用 HTML 事件处理程序,转而使用 JavaScript 指定事件处理程序的主要原因。

DOM0 事件处理程序

DOM0 事件处理程序支持在 JavaScript 中指定事件处理程序:把一个函数赋值给 DOM 元素的一个事件处理程序属性。

// 先取得操作对象的 DOM 引用
let btn = document.getElementById("myBtn"); 

// 再对 DOM 对象的事件处理程序属性赋值回调函数
btn.onclick = function () {
  console.log("Clicked");
}

// 通过将事件处理程序属性赋值为 null,可以移除 DOM0 或 HTML 方式添加的事件处理程序
btn.onclick = null;
  1. 作为事件处理程序的函数可以访问全局作用域中的一切变量
  2. 事件处理程序会被传入一个 event 对象
  3. this 值被指向所在的目标元素 (符合 this 的隐式绑定)

DOM0 事件处理程序解决了 HTML 事件处理程序中 HTML 与 JavaScript 强耦合的问题。此外,DOM0 方式在元素渲染完成后,事件处理程序赋值前,用户交互不会报错但没有反应。

DOM2 事件处理程序

DOM2 Events 规范为事件处理程序的赋值和移除定义了两个方法:

  1. addEventListener(type, handler, useCapture)
  2. removeEventLIstener(type, handler, useCapture)
// 先取得操作对象的 DOM 引用
let btn = document.getElementById("myBtn"); 

// 定义事件处理程序的回调函数
function print () {
  console.log("Clicked");
}

// DOM2 方式添加事件处理程序
btn.addEventListener("click", print, false)

// DOM2 方式移除事件处理程序
btn.removeEventListener("click", print, false)

这两个方法暴露在 所有 DOM 节点上,并接收 3 个参数:

注意: HTML 和 DOM0 的事件处理程序都要求是特定元素所支持的事件,而 DOM2 定义的两个事件处理程序赋值方法可暴露在所有 DOM 节点上,这是因为 DOM2 还实现了自定义事件的模拟和创建。

  1. 作为事件处理程序的函数可以访问全局作用域中的一切变量
  2. 事件处理程序会被传入一个 event 对象
  3. this 值被指向所在的目标元素

同 DOM0 方式类似,DOM2 方式绑定的事件处理程序同样在目标元素的作用域中执行,即 this 指向目标元素。

React 中的合成事件需要绑定 this 指向到类组件,这是因为 React 自己内部实现了事件代理,将所有事件委托到了顶层 document 对象,导致 this 指向出错,所以要显式绑定。

通过 addEventListener() 添加的事件处理程序只能使用 removeEventListener() 并传入与添加时同样的参数来移除,这意味着 addEventListener 添加的匿名函数是无法移除的。

调用 removeEventListener 时要注意,传入的 handler 参数要与 addEventListener 的保持一致,即两者比较返回 true,由于回调函数是引用类型,所以我们必须保证两者传入的都是同一函数引用。

DOM0 与 DOM2 的差异

  1. DOM0 事件处理程序不能重复绑定, 只能指向唯一对象(重复绑定事件会覆盖之前的赋值,只会使最后绑定的事件响应); DOM2 的 addEventListener 可以给一个对象注册多个 listener(重复绑定事件会依次从上到下响应)
    const btn = document.getElementById('btn');
    btn.onclick = function() {console.log(1)};
    btn.onclick = function() {console.log(2)};
    btn.addEventListener('click', function() {console.log(3)}, false);
    btn.addEventListener('click', function() {console.log(4)}, false);
    // 2
    // 3
    // 4
  2. DOM0 事件流通过事件冒泡处理, DOM2 可自定义事件流处理方式;
  3. DOM0 级处理事件, 作用于 HTML 对象, DOM2 级处理事件, 作用于任何对象;

IE 事件处理程序

IE 为事件处理程序的赋值和移除定义了两个方法:

方法接受两个参数:

由于 IE8 及更早版本只支持事件冒泡,所以通过 IE 方式添加的事件处理程序会添加到冒泡阶段

const btn = document.getElementById('btn');
btn.attachEvent('click', function() {console.log(3)});
btn.attachEvent('click', function() {console.log(4)});
// 4
// 3

IE 与 DOM0 / DOM2 异同

事件处理程序的作用域

IE 添加的事件处理程序作用域是在全局作用域中运行,this 指向 window 对象。 DOM 级方式添加的事件处理程序作用域是目标元素的作用,this 指向目标元素对象。

事件流

IE 和 DOM0 方式添加的事件处理程序都被添加到事件的冒泡阶段进行。 DOM2 可自定义事件流处理方式,默认在冒泡阶段执行事件处理程序。

重复事件处理程序处理方式

DOM0 不支持重复事件的连续触发,重复事件会覆盖上一次的事件定义,从而只会响应最后一次定义的事件处理程序。 DOM2 可以为同一事件添加多个事件处理程序,并按照添加顺序触发事件处理程序。 IE 可以为同一事件添加多个事件处理程序,但按照添加的顺序反向触发。

jtwang7 commented 3 years ago

简单实现跨浏览器事件处理程序

目的:兼容 DOM0 / DOM2 / IE 方式添加事件处理程序

const EvnetUtil = {
  addHandler: function (element, type, handler) {
    if (element.addEventListener) {
      element.addEventListener(type, handler, false);
    } else if (element.attachEvent) {
      element.attachEvent('on' + type, handler);
    } else {
      element['on' + type] = handler;
    }
  },
  removeHandler: function (element, type, handler) {
    if (element.removeEventListener) {
      element.removeEventListener(type, handler, false);
    } else if (element.detachEvent) {
      element.detachEvent('on' + type, handler);
    } else {
      element['on' + type] = null;
    }
  }
}

首先检测传入元素是否存在 DOM2 方式,若存在则使用该方式;否则检查是否存在 IE 方式,若存在则使用该方式。若前两种方式浏览器都不支持,则使用 DOM0 方式 (现代浏览器中不会执行到这一步)。

注意: IE 和 DOM0 级事件处理程序需要传入的是事件处理程序的名称,而不是事件类型,即必须在事件类型前加上 "on"。

存在的问题

上述兼容方法并没有解决所有跨浏览器一致性的问题,例如 IE 的作用域问题、多个事件处理程序执行顺序问题,DOM0 级不支持为一个事件添加多个处理程序等。