UNPKG

gs-events

Version:

事件分发器(同步)

426 lines (378 loc) 19 kB
/// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /// @Copyright ~2018 ☜Samlv9☞ and other contributors /// @MIT-LICENSE | 1.0.0 | https://api.guless.com/ /// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /// }| /// }| /// }|   へ    /| /// _______ _______ ______ }| / │   / / /// / ___ | |_ __ \ .' ____ '. }| │ Z _,< /   /`ヽ /// | (__ \_| | |__) | | (____) | }| │     ヽ   /  〉 /// '.___`-. | __ / '_.____. | }| Y     `  /  / /// |`\____) | _| | \ \_ | \____| | }| イ● 、 ●  ⊂⊃〈  / /// |_______.' |____| |___| \______,' }| ()  v    | \〈 /// |=========================================\|  >ー 、_  ィ  │ // /// |> LESS IS MORE || / へ   / ノ<|\\ /// `=========================================/| ヽ_ノ  (_/  │// /// }| 7       |/ /// }| >―r ̄ ̄`ー―_` /// }| /// }| /// Permission is hereby granted, free of charge, to any person obtaining a copy /// of this software and associated documentation files (the "Software"), to deal /// in the Software without restriction, including without limitation the rights /// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell /// copies of the Software, and to permit persons to whom the Software is /// furnished to do so, subject to the following conditions: /// /// The above copyright notice and this permission notice shall be included in all /// copies or substantial portions of the Software. /// /// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR /// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, /// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE /// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER /// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, /// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN /// THE SOFTWARE. import IEventDispatcher from "./IEventDispatcher"; import IEventListener from "./IEventListener"; import IEventListenerOptions from "./IEventListenerOptions"; import Event from "./Event"; import EventPhase from "./EventPhase"; import EventListener from "./EventListener"; /** * 定义所有具备事件派发功能对象的基类。 * @implements {IEventDispatcher} * @since 1.0.0 */ export default class EventDispatcher /*< implements IEventDispatcher >*/ { /** * 创建一个事件派发器。 * @param {IEventDispatcher} [target=null] - 指定派发器的代理目标对象。 * @since 1.0.0 */ constructor( target = null ) { /** * @type {IEventDispatcher} */ this._targetDispatcher = (target || this); /** * @type { {[type:String|Symbol]:Array<EventListener>} } */ this._listenerRegister = {}; /** * @type { {type:String|Symbol}: Boolean } */ this._listenerLockers = {}; /** 事件代理 */ if ( this !== this._targetDispatcher ) { if ( this._targetDispatcher.hasEventListener || this._targetDispatcher.addEventListener || this._targetDispatcher.removeEventListener || this._targetDispatcher.dispatchEvent ) { throw new TypeError("无法为目标对象创建代理,因为其自身已经实现了一套事件系统。"); } this._targetDispatcher.addEventListener = this.addEventListener .bind(this); this._targetDispatcher.removeEventListener = this.removeEventListener .bind(this); this._targetDispatcher.hasEventListener = this.hasEventListener .bind(this); this._targetDispatcher.dispatchEvent = this.dispatchEvent .bind(this); this._targetDispatcher._dispatchToListeners = this._dispatchToListeners.bind(this); } } /** * 注册一个事件侦听器。 * * 如果浏览器支持 {@link Symbol} 类型,则事件类型可以使用 {@link Symbol} 对象,这样可以防止 * 你的事件被其他人意外的移除。 * * 事件侦听器可以注册到派发事件的任何一个阶段,**捕获阶段**和**冒泡阶段**的侦听器是相互独立的。 * 因此使用不同的 `useCapture` 值将注册不同的侦听器。同样移除的时候,也需要指定不同的 `useCapture` 值。 * * 事件处理函数默认的作用域为派发器(`EventDispatcher`)对象或者其代理的对象,通过设置 `options.scope` * 可以为事件处理函数指定作用域。但是如果注册的是一个实现了 {@link IEventListener} 接口的对象,则直接 * 调用其 `handleEvent()` 方法处理事件,并且作用域始终指向该 `IEventListener` 对象。 * * 通过指定 `options.once` 选项,可以指定侦听器在执行后,是否自动从列表中移除。 * * 优先级高(`options.priority`)的侦听器会在派发事件时优先调用。相同优先级的情况会按照注册的先后 * 顺序调用侦听器。分别使用不同的优先级注册相同的侦听器时,以最后一次注册的侦听器为准。 * * @param {String|Symbol} type - 事件类型。 * @param {Function|IEventListener} handler - 指定事件处理函数或者是一个事件侦听器。 * @param {Boolean|IEventListenerOptions} [options=false] - 指定侦听器配置选项。 * @example * const dispatcher = new EventDispatcher(); * * /// 在冒泡阶段注册侦听器。 * dispatcher.addEventListener("custom", ( evt ) => {}); * dispatcher.addEventListener("custom", ( evt ) => {}, false); * * /// 在捕获阶段注册侦听器。 * dispatcher.addEventListener("custom", ( evt ) => {}, true); * * /// 指定事件处理函数的作用域。 * const target = {}; * dispatcher.addEventListener("custom", ( evt ) => { * console.log(target === this); // true * }, { "scope": target }); * * /// 注册一次性的侦听器。 * dispatcher.addEventListener("custom", ( evt ) => { * /// 该函数执行后,将自动从列表中移除。 * }, { "once": true }); * * /// 尽管该侦听器后注册的,但是因为具有较高的优先级,所以会先被调用。 * dispatcher.addEventListener("custom", ( evt ) => { * console.log(1); * }, { "priority": 1 }); * * /// 相同的侦听器使用不用的优先级多次注册,以最后一次注册的优先级为准。 * const handler = ( evt ) => {}; * dispatcher.addEventListener("custom", handler, { "priority": 1 }); * dispatcher.addEventListener("custom", handler, { "priority": 2 }); // 只会注册一个优先级为 2 的侦听器。 * @see {@link IEventListenerOptions} * @since 1.0.0 */ addEventListener( type, handler, options = false ) { if ( !handler ) { throw new TypeError("handler 必须为事件处理函数(function)或者是事件侦听器(IEventListener)。"); } const listener = new EventListener(handler, options); /** * 在事件派发过程中修改侦听器列表会导致派发顺序错误,因此这里需要拷贝侦听器列表的副本。 * * @example * const dispatcher = new EventDispatcher(); * dispatcher.addEventListener("custom", () => { // 如果不锁定侦听器列表的化,这里将产生一个死循环。 * console.log(1}; * dispatcher.addEventListener("custom", () => {}, { "priority": 1 }); * }); * dispatcher.dispatchEvent(new Event("custom", false, false)); */ if ( this._listenerLockers[type] ) { this._listenerLockers[type] = false; this._listenerRegister[type] = this._listenerRegister[type].slice(0); } /// 移除重复的事件侦听器。 this.removeEventListener(type, handler, listener.options.useCapture); /// 没有为 type 类型的事件注册侦听器。 if ( !this.hasEventListener(type) ) { this._listenerRegister[type] = [listener]; return; } /// 为 type 类型已经注册的侦听器列表。 const items = this._listenerRegister[type]; /** * 一般情况下,我们注册的侦听器的优先级都是默认的 0。因此这里可以先对比插入的侦听器的优先级是否为最低的优先级。 * 如果是则直接放入列表末尾。从而减少为插入侦听器而遍历列表的次数。 */ if ( items[items.length - 1].options.priority >= listener.options.priority ) { items.push(listener); return; } /** * 按照侦听器注册的顺序以及侦听器优先级的大小插入列表。 * - 1, 优先越高的侦听器放到列表的前面。 * - 2, 相同优先级的情况,先注册的侦听器放到列表的前面。 */ let insertAt = items.length - 1; while( ((insertAt >= 0) && (items[insertAt].options.priority < listener.options.priority)) ) { --insertAt; } items.splice(1 + insertAt, 0, listener); } /** * 移除一个事件侦听器。 * * 第三个参数 `useCapture` 可以是一个 {@link Boolean} 类型的值,或者是一个 {@link IEventListenerOptions} 类型的值。 * * 只有完全相同(使用 === 对比)的侦听器才会被移除。因此使用 `useCapture=false` 并不会移除**捕获阶段**的侦听器。 * 同理 `useCapture=true` 也不会移除**冒泡阶段**的侦听器。 * * @param {String|Symbol} type - 事件类型。 * @param {Function|IEventListener} handler - 指定要移除的事件处理函数或者事件侦听器。 * @param {Boolean|IEventListenerOptions} [useCapture=false] - 指定是移除捕获阶段(`true`)还是冒泡阶段(`false`)的事件侦听器。 * @example * /// 使用不用类型的 useCapture 值移除侦听器。 * dispatcher.removeEventListener(type, handler, false); * dispatcher.removeEventListener(type, handler, { "useCapture": true }); * * /// 分别移除不用阶段的侦听器。 * const dispatcher = new EventDispatcher(); * * function captureHandler( evt ) { * console.log("capture"); * } * * dispatcher.addEventListener("custom", captureHandler, true); * dispatcher.removeEventListener("custom", captureHandler, false); // 这里并不会移除 captureHandler 侦听器。 * dispatcher.hasEventListener("custom"); // true * dispatcher.removeEventListener("custom", captureHandler, true); // 这里才会移除 captureHandler 侦听器。 * dispatcher.hasEventListener("custom"); // false * @since 1.0.0 */ removeEventListener( type, handler, useCapture = false ) { if ( !this.hasEventListener(type) ) { return; } /** * 如果 `useCapture` 是一个 {@link IEventListenerOptions} 对象,则获取 `IEventListenerOptions.useCapture` 的值。 */ if ( typeof useCapture != "boolean" ) { /** @type {IEventListenerOptions} */ const options = useCapture; useCapture = (options.useCapture !== void 0 ? options.useCapture : false); } /** * 在事件派发过程中修改侦听器列表会导致派发顺序错误,因此这里需要拷贝侦听器列表的副本。 * @see {@link EventDispatcher#addEventListener} */ if ( this._listenerLockers[type] ) { this._listenerLockers[type] = false; this._listenerRegister[type] = this._listenerRegister[type].slice(0); } for ( let i = 0, items = this._listenerRegister[type]; i < items.length; ++i ) { /** * 只有完全相同(使用 === 对比)的侦听器才会被移除。 */ if ( handler === items[i].handler && useCapture === items[i].options.useCapture ) { items.splice(i, 1); break; } } } /** * 检查是否注册了指定类型的事件侦听器。 * @param {String|Symbol} type - 事件类型。 * @example * const dispatcher = new EventDispatcher(); * dispatcher.addEventListener("custom", ( evt ) => {}, false); * * console.log(dispatcher.hasEventListener("custom")); // true * console.log(dispatcher.hasEventListener("notexists")); // false * @returns {Boolean} - 如果存在指定类型的侦听器则返回 `true`,否则返回 `false`。 * @since 1.0.0 */ hasEventListener( type ) { return (!!this._listenerRegister[type] && this._listenerRegister[type].length >= 1); } /** * 派发一个事件对象到目标对象的事件流中。 * @param {Event|String|Symbol} event - 指定派发的事件对象。 * @example * const dispatcher = new EventDispatcher(); * * dispatcher.dispatchEvent(new Event("custom", false, false)); // 派发一个不冒泡的事件。 * dispatcher.dispatchEvent(new Event("custom", true, false)); // 派发一个参与冒泡的事件。 * @returns {Boolean} - 如果事件传递到了当前目标对象,并且没有被取消默认行为。则返回 `true`,否则返回 `false`。 * @since 1.0.0 */ dispatchEvent( event ) { if ( typeof event == "string" || typeof event == "symbol" ) { event = new Event(event); } /// 该事件对象已经被派发过一次! if ( event.target || event.eventPhase !== EventPhase.NONE ) { throw new Error("同一个事件对象不能派发多次!"); } event._target = this._targetDispatcher; // 设置事件目标对象。 /** * 满足以下条件时,直接调度事件至目标阶段: * * 1) 事件不参与冒泡行为。 * 2) 目标对象没有加入显示对象列表。 */ if ( !event.bubbles || !event.target.parent /*< 防止根级别对象循环引用 >*/ || (event.target === event.target.parent) ) { event._eventPhase = EventPhase.AT_TARGET; event._currentTarget = event.target; if ( event.currentTarget._dispatchToListeners ) { event.currentTarget._dispatchToListeners(event); } event._currentTarget = null; // 事件派发完成后,删除事件对当前目标的引用。 return !event.defaultPrevented; } /** * 查找事件传递路径。 */ let target = event.target; let path = [target]; while( target.parent && target !== target.parent ) { path.push(target = target.parent); } /** * 捕获阶段。 */ event._eventPhase = EventPhase.CAPTURING_PHASE; for ( let i = path.length - 1; i >= 1 && !event._stopPropagation; --i ) { event._currentTarget = path[i]; if ( event.currentTarget._dispatchToListeners ) { event.currentTarget._dispatchToListeners(event); } } if ( event._stopPropagation ) { // 事件在捕获阶段被中断。 event._currentTarget = null; return false; } /** * 目标阶段。 */ event._eventPhase = EventPhase.AT_TARGET; event._currentTarget = path[0]; if ( event.currentTarget._dispatchToListeners ) { event.currentTarget._dispatchToListeners(event); } if ( event._stopPropagation ) { event._currentTarget = null; return !event.defaultPrevented; } /** * 冒泡阶段。 */ event._eventPhase = EventPhase.BUBBLING_PHASE; for ( let i = 1; i < path.length && !event._stopPropagation; ++i ) { event._currentTarget = path[i]; if ( event.currentTarget._dispatchToListeners ) { event.currentTarget._dispatchToListeners(event); } } event._currentTarget = null; return !event.defaultPrevented; } /** * 执行所有的侦听器。 * @param {Event} event - 指定派发的事件对象。 */ _dispatchToListeners( event ) { if ( !this.hasEventListener(event.type) ) { return; } /** * 锁定侦听器列表。 */ this._listenerLockers[event.type] = true; /** * 执行侦听器 */ for ( let i = 0, items = this._listenerRegister[event.type]; i < items.length && !event._stopImmediatePropagation; ++i ) { /** @type {EventListener} */ const listener = items[i]; switch( event.eventPhase ) { case EventPhase.CAPTURING_PHASE : if ( listener.options.useCapture ) { listener.handleEvent(event); } break; case EventPhase.AT_TARGET : listener.handleEvent(event); break; case EventPhase.BUBBLING_PHASE : if ( !listener.options.useCapture ) { listener.handleEvent(event); } break; default: break; } /** * 自动移除一次性的侦听器。 */ if ( listener.options.once ) { this.removeEventListener(event.type, listener.handler, listener.options.useCapture); } } /** * 解锁侦听器列表。 */ this._listenerLockers[event.type] = false; } }