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

JavaScript自定义事件的创建与触发

2023-03-191.6k 阅读

JavaScript 自定义事件的基础概念

在 JavaScript 中,事件是文档或浏览器窗口中发生的特定交互或状态变化。我们熟悉的有鼠标点击、键盘按键按下等原生事件。而自定义事件,是开发者根据自身业务需求创建的特殊事件。

自定义事件为 JavaScript 编程带来了更高的灵活性和可扩展性。它允许我们在应用程序的不同部分之间建立松散耦合的通信机制。通过自定义事件,一个模块或组件可以在特定的条件满足时发出信号,其他对该信号感兴趣的模块或组件可以监听并作出相应的反应。

自定义事件的优势

  1. 解耦代码:在大型应用中,不同模块之间可能需要相互通信。传统的方式可能是直接调用其他模块的函数,这样会导致模块之间紧密耦合。使用自定义事件,模块只需要关心事件的发布和监听,而不需要知道具体是哪个模块触发了事件,从而降低了模块间的依赖。
  2. 增强可维护性:当需求发生变化时,如果使用自定义事件,只需要修改事件的发布或监听逻辑,而不需要在整个代码库中查找和修改模块间的直接调用关系。
  3. 提高代码复用性:自定义事件可以在不同的场景中复用。例如,一个通用的组件可能会触发自定义事件,多个不同的页面或应用模块都可以监听这个事件并执行各自的逻辑。

创建自定义事件

使用 CustomEvent 构造函数

在现代 JavaScript 中,创建自定义事件最常用的方式是使用 CustomEvent 构造函数。CustomEvent 构造函数接受两个参数:事件名称和一个可选的配置对象。

以下是一个简单的示例:

// 创建一个自定义事件
const myCustomEvent = new CustomEvent('myCustomEvent', {
  detail: {
    message: '这是来自自定义事件的详细信息'
  }
});

在上述代码中,我们创建了一个名为 myCustomEvent 的自定义事件。通过配置对象中的 detail 属性,我们可以传递任意的数据给事件的监听者。detail 属性是一个对象,你可以在其中设置各种属性和值。

兼容旧浏览器

虽然 CustomEvent 构造函数在现代浏览器中得到了很好的支持,但在一些旧浏览器中可能不存在。为了兼容这些旧浏览器,我们可以手动创建一个 CustomEvent 模拟对象。

(function () {
  if (typeof window.CustomEvent === 'function') return false;

  function CustomEvent(event, params) {
    params = params || { bubbles: false, cancelable: false, detail: undefined };
    const evt = document.createEvent('CustomEvent');
    evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
    return evt;
  }

  CustomEvent.prototype = window.Event.prototype;

  window.CustomEvent = CustomEvent;
})();

上述代码首先检查 window.CustomEvent 是否已经存在,如果不存在,则定义一个模拟的 CustomEvent 函数。这个模拟函数通过 document.createEvent('CustomEvent') 创建一个自定义事件对象,并使用 initCustomEvent 方法初始化事件的属性,如是否冒泡、是否可取消以及详细信息等。最后,将模拟的 CustomEvent 函数的原型设置为 window.Event.prototype,以确保其行为与原生的 CustomEvent 尽可能相似。

触发自定义事件

在 DOM 元素上触发

一旦创建了自定义事件,我们就需要在合适的时机触发它。最常见的场景是在 DOM 元素上触发自定义事件。

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <title>在 DOM 元素上触发自定义事件</title>
</head>

<body>
  <div id="myDiv">点击我触发自定义事件</div>

  <script>
    const myDiv = document.getElementById('myDiv');
    const myCustomEvent = new CustomEvent('myCustomEvent', {
      detail: {
        message: '这是来自自定义事件的详细信息'
      }
    });

    myDiv.addEventListener('myCustomEvent', function (e) {
      console.log('监听到自定义事件,详细信息:', e.detail.message);
    });

    myDiv.addEventListener('click', function () {
      myDiv.dispatchEvent(myCustomEvent);
    });
  </script>
</body>

</html>

在上述代码中,我们首先获取了一个 idmyDivdiv 元素。然后创建了一个自定义事件 myCustomEvent。接着,我们为 myDiv 元素添加了两个事件监听器。一个是监听 myCustomEvent 自定义事件,当监听到该事件时,会在控制台打印出事件的详细信息。另一个是监听 click 原生事件,当 myDiv 被点击时,会通过 dispatchEvent 方法触发 myCustomEvent 自定义事件。

在非 DOM 对象上触发

除了在 DOM 元素上触发自定义事件,我们也可以在非 DOM 对象上触发。在 JavaScript 中,任何对象都可以作为事件目标,只要它实现了 EventTarget 接口。

// 创建一个简单的对象
const myObject = {};

// 为对象添加 EventTarget 接口的 polyfill
if (typeof myObject.addEventListener!== 'function') {
  myObject.addEventListener = function (type, callback) {
    if (!this._eventListeners) {
      this._eventListeners = {};
    }
    if (!this._eventListeners[type]) {
      this._eventListeners[type] = [];
    }
    this._eventListeners[type].push(callback);
  };
}

if (typeof myObject.removeEventListener!== 'function') {
  myObject.removeEventListener = function (type, callback) {
    if (this._eventListeners && this._eventListeners[type]) {
      this._eventListeners[type] = this._eventListeners[type].filter(cb => cb!== callback);
    }
  };
}

if (typeof myObject.dispatchEvent!== 'function') {
  myObject.dispatchEvent = function (event) {
    if (this._eventListeners && this._eventListeners[event.type]) {
      this._eventListeners[event.type].forEach(callback => callback(event));
    }
  };
}

// 创建自定义事件
const myCustomEvent = new CustomEvent('myCustomEvent', {
  detail: {
    message: '这是来自自定义事件的详细信息'
  }
});

// 为对象添加自定义事件监听器
myObject.addEventListener('myCustomEvent', function (e) {
  console.log('监听到自定义事件,详细信息:', e.detail.message);
});

// 触发自定义事件
myObject.dispatchEvent(myCustomEvent);

在上述代码中,我们首先创建了一个普通的 JavaScript 对象 myObject。由于该对象默认没有实现 EventTarget 接口,我们通过添加一些方法来模拟实现该接口,包括 addEventListenerremoveEventListenerdispatchEvent。然后创建了一个自定义事件 myCustomEvent,并为 myObject 添加了该自定义事件的监听器。最后,通过 myObject.dispatchEvent(myCustomEvent) 触发了自定义事件。

自定义事件的冒泡与捕获

理解冒泡与捕获

在 DOM 事件模型中,事件传播有两个阶段:捕获阶段和冒泡阶段。

  1. 捕获阶段:事件从 window 对象开始,沿着 DOM 树向下传播,直到到达目标元素。
  2. 冒泡阶段:事件从目标元素开始,沿着 DOM 树向上传播,直到到达 window 对象。

自定义事件同样支持冒泡和捕获机制,这使得我们可以在不同的 DOM 层次上监听和响应自定义事件。

控制自定义事件的冒泡

通过 CustomEvent 构造函数的配置对象中的 bubbles 属性,我们可以控制自定义事件是否冒泡。

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <title>自定义事件的冒泡</title>
</head>

<body>
  <div id="parent">
    <div id="child">点击我触发自定义事件</div>
  </div>

  <script>
    const child = document.getElementById('child');
    const parent = document.getElementById('parent');

    const myCustomEvent = new CustomEvent('myCustomEvent', {
      bubbles: true,
      detail: {
        message: '这是来自自定义事件的详细信息'
      }
    });

    child.addEventListener('myCustomEvent', function (e) {
      console.log('子元素监听到自定义事件,详细信息:', e.detail.message);
    });

    parent.addEventListener('myCustomEvent', function (e) {
      console.log('父元素监听到自定义事件,详细信息:', e.detail.message);
    });

    child.addEventListener('click', function () {
      child.dispatchEvent(myCustomEvent);
    });
  </script>
</body>

</html>

在上述代码中,我们创建了一个 bubbles 属性为 true 的自定义事件 myCustomEvent。当点击子元素 child 触发自定义事件时,由于事件会冒泡,父元素 parent 也会监听到该事件。如果将 bubbles 属性设置为 false,则父元素不会监听到该事件。

捕获阶段监听自定义事件

addEventListener 方法中,通过设置第三个参数 useCapturetrue,我们可以在捕获阶段监听自定义事件。

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <title>捕获阶段监听自定义事件</title>
</head>

<body>
  <div id="parent">
    <div id="child">点击我触发自定义事件</div>
  </div>

  <script>
    const child = document.getElementById('child');
    const parent = document.getElementById('parent');

    const myCustomEvent = new CustomEvent('myCustomEvent', {
      bubbles: true,
      detail: {
        message: '这是来自自定义事件的详细信息'
      }
    });

    parent.addEventListener('myCustomEvent', function (e) {
      console.log('父元素在捕获阶段监听到自定义事件,详细信息:', e.detail.message);
    }, true);

    child.addEventListener('myCustomEvent', function (e) {
      console.log('子元素监听到自定义事件,详细信息:', e.detail.message);
    });

    child.addEventListener('click', function () {
      child.dispatchEvent(myCustomEvent);
    });
  </script>
</body>

</html>

在上述代码中,父元素 parent 在捕获阶段监听 myCustomEvent 自定义事件。当点击子元素触发自定义事件时,父元素会在捕获阶段首先监听到该事件,然后子元素会在目标阶段监听到该事件。

自定义事件的应用场景

组件间通信

在前端开发中,组件化是一种常见的开发模式。不同组件之间可能需要相互通信。例如,在一个复杂的表单组件中,可能有多个子组件,如输入框、下拉框等。当某个子组件的值发生变化时,可能需要通知其他子组件或父组件进行相应的更新。

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <title>组件间通信</title>
</head>

<body>
  <div id="formComponent">
    <input type="text" id="inputComponent">
    <div id="resultComponent">结果显示区域</div>
  </div>

  <script>
    const inputComponent = document.getElementById('inputComponent');
    const resultComponent = document.getElementById('resultComponent');

    const inputChangeEvent = new CustomEvent('inputChange', {
      bubbles: true,
      detail: {}
    });

    inputComponent.addEventListener('input', function () {
      inputChangeEvent.detail.value = this.value;
      this.dispatchEvent(inputChangeEvent);
    });

    resultComponent.addEventListener('inputChange', function (e) {
      this.textContent = '输入的值为:' + e.detail.value;
    });
  </script>
</body>

</html>

在上述代码中,我们模拟了一个表单组件,其中包含一个输入框 inputComponent 和一个结果显示区域 resultComponent。当输入框的值发生变化时,会触发一个自定义事件 inputChange,并将输入的值作为详细信息传递。结果显示区域监听该自定义事件,并根据接收到的值更新显示内容。

状态管理

在应用程序中,状态管理是一个重要的部分。自定义事件可以用于通知应用程序的不同部分状态发生了变化。

// 模拟一个简单的状态管理对象
const state = {
  count: 0
};

// 创建一个自定义事件
const stateChangeEvent = new CustomEvent('stateChange', {
  bubbles: true,
  detail: {}
});

// 为状态管理对象添加监听器
const stateChangeListener = function (e) {
  console.log('状态发生变化,新状态:', e.detail);
};
document.addEventListener('stateChange', stateChangeListener);

// 定义一个函数来更新状态并触发事件
function updateState(newState) {
  Object.assign(state, newState);
  stateChangeEvent.detail = state;
  document.dispatchEvent(stateChangeEvent);
}

// 更新状态
updateState({ count: 1 });

在上述代码中,我们创建了一个简单的状态管理对象 state。当通过 updateState 函数更新状态时,会触发一个自定义事件 stateChange,并将新的状态作为详细信息传递。任何对状态变化感兴趣的部分,都可以通过监听 stateChange 事件来获取最新的状态。

跨模块通信

在大型 JavaScript 应用中,通常会有多个模块。不同模块之间可能需要进行通信。自定义事件可以作为一种轻量级的跨模块通信方式。

// 模块 A
const moduleA = (function () {
  const myCustomEvent = new CustomEvent('moduleAEvent', {
    bubbles: true,
    detail: {
      message: '来自模块 A 的消息'
    }
  });

  function doSomething() {
    document.dispatchEvent(myCustomEvent);
  }

  return {
    doSomething: doSomething
  };
})();

// 模块 B
const moduleB = (function () {
  document.addEventListener('moduleAEvent', function (e) {
    console.log('模块 B 接收到来自模块 A 的事件,详细信息:', e.detail.message);
  });

  return {};
})();

// 调用模块 A 的方法触发事件
moduleA.doSomething();

在上述代码中,模块 A 定义了一个自定义事件 moduleAEvent,并在 doSomething 方法中触发该事件。模块 B 监听 moduleAEvent 事件,当模块 A 触发事件时,模块 B 可以接收到并执行相应的逻辑。

自定义事件的注意事项

事件命名规范

在创建自定义事件时,为了避免与原生事件或其他自定义事件冲突,应该遵循一定的命名规范。通常,使用一个特定的前缀来标识自定义事件是一个好的做法。例如,可以使用应用程序或模块的名称作为前缀,如 app:myCustomEventmodule1:customAction

内存泄漏问题

如果不正确地使用自定义事件,可能会导致内存泄漏。特别是在使用 addEventListener 添加事件监听器时,如果没有及时移除监听器,当对象被销毁时,监听器仍然会保留在内存中,导致无法释放相关资源。

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <title>内存泄漏示例</title>
</head>

<body>
  <div id="myDiv">点击我添加监听器</div>

  <script>
    const myDiv = document.getElementById('myDiv');
    let myObject;

    myDiv.addEventListener('click', function () {
      myObject = {
        name: '示例对象'
      };

      myObject.addEventListener = function (type, callback) {
        if (!this._eventListeners) {
          this._eventListeners = {};
        }
        if (!this._eventListeners[type]) {
          this._eventListeners[type] = [];
        }
        this._eventListeners[type].push(callback);
      };

      myObject.dispatchEvent = function (event) {
        if (this._eventListeners && this._eventListeners[event.type]) {
          this._eventListeners[event.type].forEach(callback => callback(event));
        }
      };

      const myCustomEvent = new CustomEvent('myCustomEvent');

      myObject.addEventListener('myCustomEvent', function () {
        console.log('对象监听到自定义事件');
      });

      myObject.dispatchEvent(myCustomEvent);

      // 假设这里 myObject 不再使用,但没有移除监听器
      myObject = null;
    });
  </script>
</body>

</html>

在上述代码中,当点击 myDiv 时,创建了一个 myObject 对象,并为其添加了一个自定义事件监听器。之后,虽然将 myObject 设置为 null,但由于没有移除监听器,监听器仍然保留在内存中,可能会导致内存泄漏。为了避免这种情况,应该在适当的时候调用 removeEventListener 方法移除监听器。

事件触发频率

如果频繁地触发自定义事件,可能会导致性能问题。特别是在一些性能敏感的场景中,如动画、滚动等,需要谨慎控制事件的触发频率。可以使用防抖(Debounce)或节流(Throttle)技术来限制事件的触发频率。

// 防抖函数
function debounce(func, delay) {
  let timer;
  return function () {
    const context = this;
    const args = arguments;
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(context, args);
    }, delay);
  };
}

// 节流函数
function throttle(func, delay) {
  let lastTime = 0;
  return function () {
    const context = this;
    const args = arguments;
    const now = new Date().getTime();
    if (now - lastTime >= delay) {
      func.apply(context, args);
      lastTime = now;
    }
  };
}

// 假设这里有一个频繁触发自定义事件的函数
function frequentlyTriggerEvent() {
  const myCustomEvent = new CustomEvent('myCustomEvent');
  document.dispatchEvent(myCustomEvent);
}

// 使用防抖函数包装
const debouncedTrigger = debounce(frequentlyTriggerEvent, 200);

// 使用节流函数包装
const throttledTrigger = throttle(frequentlyTriggerEvent, 200);

在上述代码中,我们定义了防抖和节流函数。通过将频繁触发自定义事件的函数用防抖或节流函数包装,可以有效地控制事件的触发频率,提高性能。