Source/Core/Event.js

js 复制代码
import Check from "./Check.js";
import defined from "./defined.js";

/**
 * A generic utility class for managing subscribers for a particular event.
 * This class is usually instantiated inside of a container class and
 * exposed as a property for others to subscribe to.
 *
 * @alias Event
 * @template Listener extends (...args: any[]) => void = (...args: any[]) => void
 * @constructor
 * @example
 * MyObject.prototype.myListener = function(arg1, arg2) {
 *     this.myArg1Copy = arg1;
 *     this.myArg2Copy = arg2;
 * }
 *
 * const myObjectInstance = new MyObject();
 * const evt = new Cesium.Event();
 * evt.addEventListener(MyObject.prototype.myListener, myObjectInstance);
 * evt.raiseEvent('1', '2');
 * evt.removeEventListener(MyObject.prototype.myListener);
 */
function Event() {
  /**
   * @type {Map<Listener,Set<object>>}
   * @private
   */
  this._listeners = new Map();
  /**
   * @type {Map<Listener,Set<object>>}
   * @private
   */
  this._toRemove = new Map();
  /**
   * @type {Map<Listener,Set<object>>}
   * @private
   */
  this._toAdd = new Map();
  this._invokingListeners = false;
  this._listenerCount = 0; // Tracks number of listener + scope pairs
}

Object.defineProperties(Event.prototype, {
  /**
   * The number of listeners currently subscribed to the event.
   * @memberof Event.prototype
   * @type {number}
   * @readonly
   */
  numberOfListeners: {
    get: function () {
      return this._listenerCount;
    },
  },
});

/**
 * Registers a callback function to be executed whenever the event is raised.
 * An optional scope can be provided to serve as the <code>this</code> pointer
 * in which the function will execute.
 *
 * @param {Listener} listener The function to be executed when the event is raised.
 * @param {object} [scope] An optional object scope to serve as the <code>this</code>
 *        pointer in which the listener function will execute.
 * @returns {Event.RemoveCallback} A function that will remove this event listener when invoked.
 *
 * @see Event#raiseEvent
 * @see Event#removeEventListener
 */
Event.prototype.addEventListener = function (listener, scope) {
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.func("listener", listener);
  //>>includeEnd('debug');
  const event = this;

  const listenerMap = event._invokingListeners
    ? event._toAdd
    : event._listeners;
  const added = addEventListener(this, listenerMap, listener, scope);
  if (added) {
    event._listenerCount++;
  }

  return function () {
    event.removeEventListener(listener, scope);
  };
};

function addEventListener(event, listenerMap, listener, scope) {
  if (!listenerMap.has(listener)) {
    listenerMap.set(listener, new Set());
  }
  const scopes = listenerMap.get(listener);
  if (!scopes.has(scope)) {
    scopes.add(scope);
    return true;
  }

  return false;
}

/**
 * Unregisters a previously registered callback.
 *
 * @param {Listener} listener The function to be unregistered.
 * @param {object} [scope] The scope that was originally passed to addEventListener.
 * @returns {boolean} <code>true</code> if the listener was removed; <code>false</code> if the listener and scope are not registered with the event.
 *
 * @see Event#addEventListener
 * @see Event#raiseEvent
 */
Event.prototype.removeEventListener = function (listener, scope) {
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.func("listener", listener);
  //>>includeEnd('debug');

  const removedFromListeners = removeEventListener(
    this,
    this._listeners,
    listener,
    scope,
  );
  const removedFromToAdd = removeEventListener(
    this,
    this._toAdd,
    listener,
    scope,
  );

  const removed = removedFromListeners || removedFromToAdd;
  if (removed) {
    this._listenerCount--;
  }

  return removed;
};

function removeEventListener(event, listenerMap, listener, scope) {
  const scopes = listenerMap.get(listener);
  if (!scopes || !scopes.has(scope)) {
    return false;
  }

  if (event._invokingListeners) {
    if (!addEventListener(event, event._toRemove, listener, scope)) {
      // Already marked for removal
      return false;
    }
  } else {
    scopes.delete(scope);
    if (scopes.size === 0) {
      listenerMap.delete(listener);
    }
  }

  return true;
}

/**
 * Raises the event by calling each registered listener with all supplied arguments.
 *
 * @param {...Parameters<Listener>} arguments This method takes any number of parameters and passes them through to the listener functions.
 *
 * @see Event#addEventListener
 * @see Event#removeEventListener
 */
Event.prototype.raiseEvent = function () {
  this._invokingListeners = true;

  for (const [listener, scopes] of this._listeners.entries()) {
    if (!defined(listener)) {
      continue;
    }

    for (const scope of scopes) {
      listener.apply(scope, arguments);
    }
  }

  this._invokingListeners = false;

  // Actually add items marked for addition
  for (const [listener, scopes] of this._toAdd.entries()) {
    for (const scope of scopes) {
      addEventListener(this, this._listeners, listener, scope);
    }
  }
  this._toAdd.clear();

  // Actually remove items marked for removal
  for (const [listener, scopes] of this._toRemove.entries()) {
    for (const scope of scopes) {
      removeEventListener(this, this._listeners, listener, scope);
    }
  }
  this._toRemove.clear();
};

/**
 * A function that removes a listener.
 * @callback Event.RemoveCallback
 */

export default Event;

这段代码实现了一个通用的事件(观察者)系统 ,用于管理事件监听器的注册、移除和触发。它允许任意对象通过 Event 实例来发布事件,其他对象可以订阅该事件并在事件触发时收到通知。


整体结构

js 复制代码
function Event() {
  this._listeners = new Map();    // 当前有效的监听器
  this._toRemove = new Map();     // 等待移除的监听器(在事件触发过程中)
  this._toAdd = new Map();        // 等待添加的监听器(在事件触发过程中)
  this._invokingListeners = false; // 是否正在触发事件
  this._listenerCount = 0;        // 监听器总数(计数每个监听器+scope组合)
}
  • _listeners :存储实际有效的监听器。键是监听器函数,值是一个 Set 对象,包含该监听器绑定的所有 scope(即 this 上下文)。
  • _toAdd / _toRemove :在事件触发过程中,如果添加或移除监听器,不会直接修改 _listeners,而是暂存到这两个临时容器中,等事件触发完毕后再批量处理。
  • _invokingListeners :标志位,表示当前是否正在执行 raiseEvent 调用。
  • _listenerCount :记录所有 (listener, scope) 对的总数,通过 numberOfListeners 属性暴露只读值。

核心方法

1. addEventListener(listener, scope)

注册一个监听器,可选的 scope 参数用于指定回调函数执行时的 this 值。

js 复制代码
Event.prototype.addEventListener = function (listener, scope) {
  Check.typeOf.func("listener", listener);
  const event = this;
  const listenerMap = event._invokingListeners ? event._toAdd : event._listeners;
  const added = addEventListener(this, listenerMap, listener, scope);
  if (added) {
    event._listenerCount++;
  }
  return function () {
    event.removeEventListener(listener, scope);
  };
};
  • 参数校验 :使用之前定义的 Check.typeOf.func 确保传入的监听器是函数。
  • 选择存储位置 :如果当前正在触发事件(_invokingListeners === true),则新注册的监听器会被临时放入 _toAdd,否则直接放入 _listeners
  • 辅助函数 addEventListener :检查是否已经存在相同的 (listener, scope) 对,若不存在则添加并返回 true,否则返回 false。只有当真正添加时,_listenerCount 才会增加。
  • 返回值:返回一个"移除函数",调用它即可移除该监听器,方便在需要时做一次性订阅。

2. removeEventListener(listener, scope)

移除之前注册的监听器。

js 复制代码
Event.prototype.removeEventListener = function (listener, scope) {
  Check.typeOf.func("listener", listener);
  const removedFromListeners = removeEventListener(this, this._listeners, listener, scope);
  const removedFromToAdd = removeEventListener(this, this._toAdd, listener, scope);
  const removed = removedFromListeners || removedFromToAdd;
  if (removed) {
    this._listenerCount--;
  }
  return removed;
};
  • 同样先校验监听器类型。
  • 尝试从 _listeners_toAdd 中移除该 (listener, scope) 对(因为可能之前添加时正处于触发过程中,监听器在 _toAdd 中)。
  • 辅助函数 removeEventListener 的处理逻辑与添加类似:如果当前正在触发事件,则将要移除的监听器暂存到 _toRemove 中(而不是立即从 _listeners 删除),避免在遍历过程中修改集合。如果不在触发过程中,则直接删除。

3. raiseEvent(...args)

触发事件,调用所有注册的监听器。

js 复制代码
Event.prototype.raiseEvent = function () {
  this._invokingListeners = true;

  for (const [listener, scopes] of this._listeners.entries()) {
    if (!defined(listener)) {
      continue;
    }
    for (const scope of scopes) {
      listener.apply(scope, arguments);
    }
  }

  this._invokingListeners = false;

  // 批量添加在触发过程中新注册的监听器
  for (const [listener, scopes] of this._toAdd.entries()) {
    for (const scope of scopes) {
      addEventListener(this, this._listeners, listener, scope);
    }
  }
  this._toAdd.clear();

  // 批量移除在触发过程中被标记为移除的监听器
  for (const [listener, scopes] of this._toRemove.entries()) {
    for (const scope of scopes) {
      removeEventListener(this, this._listeners, listener, scope);
    }
  }
  this._toRemove.clear();
};
  • 设置标志 :开始时将 _invokingListeners 设为 true,此时任何对 addEventListener / removeEventListener 的调用都会使用临时容器 _toAdd / _toRemove
  • 遍历当前监听器 :使用 for...of 遍历 _listeners 中的每个监听器及其作用域集合,通过 listener.apply(scope, arguments) 调用,其中 argumentsraiseEvent 传入的所有参数。
  • 处理临时容器 :事件触发完成后,将 _toAdd 中的监听器合并到 _listeners,将 _toRemove 中的监听器从 _listeners 中移除,然后清空两个临时容器。
  • 避免并发修改 :这种设计保证了在遍历 _listeners 的过程中,监听器的集合不会被意外修改,从而避免了 ConcurrentModificationException 类的问题。

设计亮点

1. 支持同一个监听器绑定多个 scope

通过 Map<Listener, Set<scope>> 的结构,允许多个对象使用同一个函数监听事件,且每个对象拥有独立的 this 上下文。例如:

js 复制代码
const handler = function(val) { console.log(this.name, val); };
const obj1 = { name: 'A' };
const obj2 = { name: 'B' };
event.addEventListener(handler, obj1);
event.addEventListener(handler, obj2);
// 触发时,两个对象都会收到通知,各自打印自己的 name

2. 安全的事件触发

在触发过程中添加或移除监听器,不会导致遍历出错。临时容器 _toAdd_toRemove 充当缓冲,确保迭代器稳定。

3. 返回移除函数

addEventListener 返回一个函数,调用即可移除该监听器,这种模式在一次性订阅中非常方便:

js 复制代码
const remove = event.addEventListener(() => { ... });
// 之后可以随时调用 remove() 取消订阅

4. 使用 Check 进行参数校验

在开发模式下(通过 //>>includeStart 条件编译),会检查监听器是否为函数,帮助开发者及早发现错误。

5. 泛型注释

@template Listener extends (...args: any[]) => void 表明这是一个泛型类,可以在 TypeScript 环境下提供更好的类型推导。


使用示例

js 复制代码
const evt = new Event();

function onDataChanged(newData, oldData) {
  console.log(`Data changed from ${oldData} to ${newData}`);
}

// 订阅
const remove = evt.addEventListener(onDataChanged);

// 触发
evt.raiseEvent('new value', 'old value'); // 控制台输出

// 取消订阅
remove();

适用场景

Event 类非常适合作为组件或模块的事件通信基础设施。例如在三维引擎 Cesium 中,很多对象(如 ViewerDataSourceCollection)都暴露了 Event 类型的属性,供外部订阅其内部状态变化。

通过这种设计,可以轻松实现发布-订阅模式,解耦事件的触发者和监听者,提高代码的可维护性和可扩展性。

相关推荐
似水明俊德1 小时前
04-C#.Net-委托和事件-面试题
java·开发语言·面试·c#·.net
johnrui2 小时前
集合与树形结构
开发语言·windows
赵锦川2 小时前
大屏比例缩放
前端·javascript·html
该怎么办呢2 小时前
Source/Core/DeveloperError.js
开发语言·javascript·ecmascript
小璐资源网2 小时前
Java 21 新特性实战:虚拟线程详解
java·开发语言·python
m0_569881472 小时前
基于C++的数据库连接池
开发语言·c++·算法
.select.2 小时前
c++ auto
开发语言·c++·算法
2401_884563242 小时前
C++中的访问者模式高级应用
开发语言·c++·算法
消失的旧时光-19432 小时前
C++ 多态核心三件套:虚函数、纯虚函数、虚析构函数(面试 + 工程完全指南)
开发语言·c++·面试·虚函数·纯虚函数·虚析构函数