MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript事件监听的多种方式

2021-07-021.2k 阅读

传统方式:HTML 事件处理属性

在早期的 JavaScript 开发中,通过 HTML 元素的事件处理属性来绑定事件是一种很直接的方式。比如,我们想要在按钮被点击时执行一段 JavaScript 代码,可以这样写:

<!DOCTYPE html>
<html>
  <body>
    <button onclick="alert('按钮被点击了!')">点击我</button>
  </body>
</html>

在上述代码中,onclick 就是按钮元素的事件处理属性,其值是一段 JavaScript 代码。当按钮被点击时,这段代码就会被执行。

这种方式的优点在于简单直观,对于简单的交互逻辑实现起来非常便捷。然而,它也存在一些明显的缺点。首先,HTML 代码与 JavaScript 代码紧密耦合,不利于代码的维护和复用。假设项目中有多个按钮都需要相同的点击逻辑,使用这种方式就需要在每个按钮的 onclick 属性中重复编写相同的代码。其次,从代码结构角度来看,将 JavaScript 代码写在 HTML 标签内会使 HTML 文件变得臃肿,破坏了代码的清晰结构。

DOM0 级事件处理程序

DOM0 级事件处理程序是 JavaScript 中最早出现的一种在脚本中绑定事件的方式。通过获取 DOM 元素,然后直接为其指定事件处理函数来实现事件监听。

<!DOCTYPE html>
<html>
  <body>
    <button id="myButton">点击我</button>
    <script>
      const button = document.getElementById('myButton');
      button.onclick = function() {
        alert('按钮被点击了,这是 DOM0 级事件处理');
      };
    </script>
  </body>
</html>

在这段代码中,我们首先通过 document.getElementById 获取到按钮元素,然后为其 onclick 属性赋值一个函数。当按钮被点击时,这个函数就会被执行。

DOM0 级事件处理程序的优点是简单明了,兼容性好,几乎所有的浏览器都支持这种方式。它将 JavaScript 代码从 HTML 中分离出来,使得代码结构相对清晰。不过,这种方式也有局限性,一个元素的同一个事件只能绑定一个处理函数。如果需要为按钮的点击事件绑定多个不同的处理逻辑,使用 DOM0 级事件处理程序就无法满足需求。例如:

<!DOCTYPE html>
<html>
  <body>
    <button id="myButton">点击我</button>
    <script>
      const button = document.getElementById('myButton');
      button.onclick = function() {
        alert('第一个点击处理逻辑');
      };
      button.onclick = function() {
        alert('第二个点击处理逻辑');
      };
    </script>
  </body>
</html>

在上述代码中,第二个 onclick 赋值会覆盖第一个,最终只有第二个处理函数会在按钮点击时执行。

DOM2 级事件处理程序:addEventListener 和 removeEventListener

为了解决 DOM0 级事件处理程序只能绑定一个事件处理函数的问题,DOM2 级事件规范引入了 addEventListenerremoveEventListener 方法。

<!DOCTYPE html>
<html>
  <body>
    <button id="myButton">点击我</button>
    <script>
      const button = document.getElementById('myButton');
      const handleClick1 = function() {
        alert('第一个点击处理逻辑,通过 addEventListener 绑定');
      };
      const handleClick2 = function() {
        alert('第二个点击处理逻辑,通过 addEventListener 绑定');
      };
      button.addEventListener('click', handleClick1);
      button.addEventListener('click', handleClick2);
    </script>
  </body>
</html>

在这段代码中,我们通过 addEventListener 方法为按钮的 click 事件绑定了两个不同的处理函数 handleClick1handleClick2。当按钮被点击时,这两个函数会按照绑定的顺序依次执行。

addEventListener 方法接受三个参数:第一个参数是事件类型,如 'click''mouseover' 等;第二个参数是事件处理函数;第三个参数是一个布尔值,表示是否在捕获阶段处理事件,默认值为 false,即在冒泡阶段处理事件。例如:

<!DOCTYPE html>
<html>
  <body>
    <div id="outer">
      <div id="inner">点击我</div>
    </div>
    <script>
      const outer = document.getElementById('outer');
      const inner = document.getElementById('inner');
      outer.addEventListener('click', function() {
        alert('外部 div 在冒泡阶段被点击');
      }, false);
      inner.addEventListener('click', function() {
        alert('内部 div 在冒泡阶段被点击');
      }, false);
      outer.addEventListener('click', function() {
        alert('外部 div 在捕获阶段被点击');
      }, true);
      inner.addEventListener('click', function() {
        alert('内部 div 在捕获阶段被点击');
      }, true);
    </script>
  </body>
</html>

在上述代码中,当点击内部 div 时,首先会触发捕获阶段的事件处理函数,按照从外到内的顺序执行;然后会触发冒泡阶段的事件处理函数,按照从内到外的顺序执行。

addEventListener 对应的是 removeEventListener 方法,用于移除已经绑定的事件处理函数。例如:

<!DOCTYPE html>
<html>
  <body>
    <button id="myButton">点击我</button>
    <script>
      const button = document.getElementById('myButton');
      const handleClick = function() {
        alert('按钮被点击,将被移除');
      };
      button.addEventListener('click', handleClick);
      // 移除事件处理函数
      button.removeEventListener('click', handleClick);
    </script>
  </body>
</html>

需要注意的是,removeEventListener 移除事件处理函数时,传入的函数必须与 addEventListener 绑定的函数是同一个引用。如果使用匿名函数绑定事件,就无法通过 removeEventListener 移除。例如:

<!DOCTYPE html>
<html>
  <body>
    <button id="myButton">点击我</button>
    <script>
      const button = document.getElementById('myButton');
      button.addEventListener('click', function() {
        alert('匿名函数绑定的点击事件');
      });
      // 以下代码无法移除事件处理函数
      button.removeEventListener('click', function() {
        alert('匿名函数绑定的点击事件');
      });
    </script>
  </body>
</html>

在上述代码中,虽然两个匿名函数的代码看起来一样,但它们是不同的函数引用,所以无法成功移除事件处理函数。

IE 特有的事件处理方式:attachEvent 和 detachEvent

在 IE 浏览器(IE9 及以下)中,不支持 addEventListenerremoveEventListener 方法,而是使用 attachEventdetachEvent 来处理事件。

<!DOCTYPE html>
<html>
  <body>
    <button id="myButton">点击我</button>
    <script>
      const button = document.getElementById('myButton');
      const handleClick = function() {
        alert('IE 特有的事件处理方式');
      };
      if (button.attachEvent) {
        button.attachEvent('onclick', handleClick);
      } else if (button.addEventListener) {
        button.addEventListener('click', handleClick);
      }
    </script>
  </body>
</html>

在上述代码中,我们通过检测 button.attachEvent 是否存在来判断是否为 IE 浏览器。如果存在,则使用 attachEvent 方法绑定事件。attachEvent 方法接受两个参数,第一个参数是事件类型,需要加上 'on' 前缀,如 'onclick';第二个参数是事件处理函数。

attachEvent 对应的是 detachEvent 方法,用于移除事件处理函数。例如:

<!DOCTYPE html>
<html>
  <body>
    <button id="myButton">点击我</button>
    <script>
      const button = document.getElementById('myButton');
      const handleClick = function() {
        alert('将被移除的事件处理');
      };
      if (button.attachEvent) {
        button.attachEvent('onclick', handleClick);
        button.detachEvent('onclick', handleClick);
      } else if (button.addEventListener) {
        button.addEventListener('click', handleClick);
        button.removeEventListener('click', handleClick);
      }
    </script>
  </body>
</html>

IE 特有的这种事件处理方式与 DOM2 级事件处理方式有一些区别。首先,事件处理函数的执行环境不同。在 attachEvent 中,this 指向的是 window 对象,而在 addEventListener 中,this 指向的是触发事件的元素本身。例如:

<!DOCTYPE html>
<html>
  <body>
    <button id="myButton">点击我</button>
    <script>
      const button = document.getElementById('myButton');
      const handleClick1 = function() {
        console.log(this === window);
      };
      const handleClick2 = function() {
        console.log(this === button);
      };
      if (button.attachEvent) {
        button.attachEvent('onclick', handleClick1);
      } else if (button.addEventListener) {
        button.addEventListener('click', handleClick2);
      }
    </script>
  </body>
</html>

在上述代码中,使用 attachEvent 绑定的事件处理函数 handleClick1 中,this === windowtrue;而使用 addEventListener 绑定的事件处理函数 handleClick2 中,this === buttontrue

其次,事件处理函数的执行顺序也有所不同。attachEvent 绑定的事件处理函数是按照添加的相反顺序执行,而 addEventListener 是按照添加的顺序执行。例如:

<!DOCTYPE html>
<html>
  <body>
    <button id="myButton">点击我</button>
    <script>
      const button = document.getElementById('myButton');
      const handleClick1 = function() {
        alert('第一个处理函数');
      };
      const handleClick2 = function() {
        alert('第二个处理函数');
      };
      if (button.attachEvent) {
        button.attachEvent('onclick', handleClick1);
        button.attachEvent('onclick', handleClick2);
      } else if (button.addEventListener) {
        button.addEventListener('click', handleClick1);
        button.addEventListener('click', handleClick2);
      }
    </script>
  </body>
</html>

在 IE 浏览器中,点击按钮时会先弹出 '第二个处理函数',再弹出 '第一个处理函数';而在支持 addEventListener 的浏览器中,会先弹出 '第一个处理函数',再弹出 '第二个处理函数'

事件委托

事件委托是一种利用事件冒泡机制来优化事件处理的技术。假设我们有一个列表,列表中有多个列表项,每个列表项都需要绑定点击事件。如果为每个列表项都单独绑定事件处理函数,会占用较多的内存和性能。通过事件委托,我们可以将事件处理函数绑定到父元素上,利用事件冒泡来处理子元素的事件。

<!DOCTYPE html>
<html>
  <body>
    <ul id="myList">
      <li>列表项 1</li>
      <li>列表项 2</li>
      <li>列表项 3</li>
    </ul>
    <script>
      const list = document.getElementById('myList');
      list.addEventListener('click', function(event) {
        if (event.target.tagName === 'LI') {
          alert('点击了列表项:' + event.target.textContent);
        }
      });
    </script>
  </body>
</html>

在上述代码中,我们为 ul 元素绑定了 click 事件处理函数。当点击列表项时,由于事件冒泡,ul 元素会接收到点击事件。在事件处理函数中,通过判断 event.targettagName 是否为 'LI',来确定是否是列表项被点击,并获取其文本内容。

事件委托的优点有很多。首先,减少了事件处理函数的数量,提高了性能。特别是在处理大量子元素的事件时,这种优化效果更加明显。其次,动态添加的子元素也能自动拥有相同的事件处理逻辑。例如:

<!DOCTYPE html>
<html>
  <body>
    <ul id="myList">
      <li>列表项 1</li>
      <li>列表项 2</li>
      <li>列表项 3</li>
    </ul>
    <button id="addItem">添加列表项</button>
    <script>
      const list = document.getElementById('myList');
      const addItemButton = document.getElementById('addItem');
      list.addEventListener('click', function(event) {
        if (event.target.tagName === 'LI') {
          alert('点击了列表项:' + event.target.textContent);
        }
      });
      addItemButton.addEventListener('click', function() {
        const newItem = document.createElement('li');
        newItem.textContent = '新的列表项';
        list.appendChild(newItem);
      });
    </script>
  </body>
</html>

在上述代码中,点击 添加列表项 按钮会动态添加新的列表项。由于事件委托的存在,新添加的列表项同样可以响应点击事件,而无需为其单独绑定处理函数。

然而,事件委托也有一些局限性。比如,对于一些不支持冒泡的事件,如 focusblur 等,就无法使用事件委托。另外,如果事件处理逻辑较为复杂,在父元素中判断 event.target 可能会使代码变得繁琐。

跨浏览器的事件处理封装

为了在不同浏览器中统一事件处理方式,我们可以封装一个跨浏览器的事件处理函数。

const EventUtil = {
  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;
    }
  }
};

使用这个封装的 EventUtil 对象,我们可以在不同浏览器中以统一的方式绑定和移除事件处理函数。例如:

<!DOCTYPE html>
<html>
  <body>
    <button id="myButton">点击我</button>
    <script>
      const button = document.getElementById('myButton');
      const handleClick = function() {
        alert('跨浏览器事件处理');
      };
      EventUtil.addHandler(button, 'click', handleClick);
      // 移除事件处理函数
      EventUtil.removeHandler(button, 'click', handleClick);
    </script>
  </body>
</html>

在上述代码中,EventUtil.addHandler 方法会根据浏览器的类型,选择合适的方式绑定事件处理函数;EventUtil.removeHandler 方法同理,用于移除事件处理函数。这样,我们就可以在不考虑浏览器差异的情况下,方便地处理事件。

自定义事件

除了监听 DOM 元素的原生事件外,JavaScript 还支持自定义事件。自定义事件允许我们在代码中定义和触发自己的事件,以实现更灵活的组件间通信和逻辑解耦。

<!DOCTYPE html>
<html>
  <body>
    <div id="myDiv">自定义事件演示</div>
    <script>
      const div = document.getElementById('myDiv');
      // 创建自定义事件
      const myEvent = new CustomEvent('myCustomEvent', {
        detail: {
          message: '这是自定义事件携带的数据'
        }
      });
      // 监听自定义事件
      div.addEventListener('myCustomEvent', function(event) {
        alert('接收到自定义事件,数据:' + event.detail.message);
      });
      // 触发自定义事件
      div.dispatchEvent(myEvent);
    </script>
  </body>
</html>

在上述代码中,我们首先使用 new CustomEvent 创建了一个自定义事件 myCustomEvent,并通过 detail 属性传递了一些数据。然后,我们为 div 元素监听这个自定义事件,并在需要的时候通过 div.dispatchEvent(myEvent) 触发该事件。

自定义事件在很多场景下都非常有用。比如在一个复杂的 JavaScript 应用中,不同的模块之间可能需要进行通信,但又不想直接耦合在一起。通过自定义事件,一个模块可以触发事件,其他模块可以监听这些事件并做出相应的反应。例如,在一个单页应用中,当用户登录成功后,可能需要通知多个不同的组件进行界面更新等操作,这时就可以使用自定义事件来实现。

<!DOCTYPE html>
<html>
  <body>
    <button id="loginButton">登录</button>
    <div id="userInfo">用户信息区域</div>
    <div id="navigation">导航区域</div>
    <script>
      const loginButton = document.getElementById('loginButton');
      const userInfoDiv = document.getElementById('userInfo');
      const navigationDiv = document.getElementById('navigation');
      // 创建登录成功的自定义事件
      const loginSuccessEvent = new CustomEvent('loginSuccess', {
        detail: {
          username: 'JohnDoe'
        }
      });
      // 监听登录成功事件,更新用户信息
      userInfoDiv.addEventListener('loginSuccess', function(event) {
        this.textContent = '欢迎,' + event.detail.username;
      });
      // 监听登录成功事件,更新导航
      navigationDiv.addEventListener('loginSuccess', function() {
        this.innerHTML = '<a href="#">个人中心</a> <a href="#">注销</a>';
      });
      loginButton.addEventListener('click', function() {
        // 模拟登录成功
        console.log('登录成功');
        // 触发登录成功事件
        document.dispatchEvent(loginSuccessEvent);
      });
    </script>
  </body>
</html>

在上述代码中,当点击 登录 按钮时,模拟登录成功并触发 loginSuccess 自定义事件。userInfoDivnavigationDiv 分别监听这个事件,并根据事件携带的数据或执行相应的更新操作。这样,不同组件之间通过自定义事件进行了松耦合的通信。

总结不同方式的适用场景

  1. HTML 事件处理属性:适用于非常简单的页面交互,例如在一个小型的静态页面中,偶尔需要添加一个简单的按钮点击效果等。由于其代码耦合度高,不适合复杂项目和需要维护的代码。
  2. DOM0 级事件处理程序:适用于一些对兼容性要求极高,且事件处理逻辑较为单一的场景。比如一些需要兼容古老浏览器的内部工具页面,每个元素只需要一个简单的事件处理函数。
  3. DOM2 级事件处理程序(addEventListenerremoveEventListener:这是现代 JavaScript 开发中最常用的事件处理方式。适用于绝大多数场景,无论是简单的页面交互还是复杂的单页应用开发。它支持为一个元素的同一个事件绑定多个处理函数,并且能够很好地控制事件的捕获和冒泡阶段。
  4. IE 特有的事件处理方式(attachEventdetachEvent:仅适用于必须兼容 IE9 及以下浏览器的项目。由于现代浏览器对 IE 的支持逐渐减少,这种方式在新开发项目中很少使用。
  5. 事件委托:适用于处理大量相似元素的事件,或者动态添加元素需要统一事件处理逻辑的场景。比如在一个电商网站的商品列表页面,每个商品都有点击查看详情的功能,使用事件委托可以大大提高性能。
  6. 跨浏览器的事件处理封装:在需要兼容多种浏览器,且希望以统一方式处理事件的项目中非常有用。特别是在一些面向大众用户的 Web 应用中,用户可能使用各种不同的浏览器访问,这种封装可以简化代码并提高兼容性。
  7. 自定义事件:适用于复杂应用中组件间的通信和逻辑解耦。当不同模块之间需要进行信息传递,但又不想直接相互依赖时,自定义事件是一个很好的选择。例如在一个大型的 JavaScript 框架开发中,各个模块之间可以通过自定义事件进行交互。

通过深入了解和合理运用这些 JavaScript 事件监听方式,开发者可以根据项目的具体需求,选择最合适的方式来实现高效、灵活且易于维护的事件处理逻辑。无论是简单的页面交互还是复杂的大型应用开发,都能找到对应的解决方案,从而提升用户体验和开发效率。