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)调用,其中arguments是raiseEvent传入的所有参数。 - 处理临时容器 :事件触发完成后,将
_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 中,很多对象(如 Viewer、DataSourceCollection)都暴露了 Event 类型的属性,供外部订阅其内部状态变化。
通过这种设计,可以轻松实现发布-订阅模式,解耦事件的触发者和监听者,提高代码的可维护性和可扩展性。