Skip to content

Commit

Permalink
Implements EventTarget in a better way (cocos#6571)
Browse files Browse the repository at this point in the history
* Implements EventTarget in a better way

* Fix

* Update

* Use Object instead of empty class

* Comment

* Revert "Use Object instead of empty class"

This reverts commit 3d41c70.

* Eventful -> Eventify
  • Loading branch information
shrinktofit authored May 21, 2020
1 parent e91bbfe commit ac34060
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 300 deletions.
2 changes: 2 additions & 0 deletions .vscode/cSpell.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"deinterleave",
"deserialize",
"earcut",
"eventify",
"eventified",
"forin",
"glsl",
"grayscale",
Expand Down
41 changes: 8 additions & 33 deletions cocos/core/animation/animation-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@

import { Component } from '../components/component';
import { ccclass, help, executeInEditMode, executionOrder, menu, property } from '../data/class-decorator';
import { Event, EventTarget } from '../event';
import { CallbacksInvoker, ICallbackTable } from '../event/callbacks-invoker';
import { applyMixins, IEventTarget } from '../event/event-target-factory';
import { Eventify } from '../event/eventify';
import { warnID } from '../platform/debug';
import * as ArrayUtils from '../utils/array';
import { createMap } from '../utils/js-typed';
Expand Down Expand Up @@ -79,6 +77,8 @@ export enum EventType {
}
ccenum(EventType);

type AnimationEventCallback<ThisType> = (this: ThisType, type: EventType, state: AnimationState) => void;

/**
* 动画组件管理动画状态来控制动画的播放。
* 它提供了方便的接口用来预创建指定动画剪辑的动画状态,并提供了一系列事件:
Expand All @@ -94,7 +94,7 @@ ccenum(EventType);
@executionOrder(99)
@executeInEditMode
@menu('Components/Animation')
export class AnimationComponent extends Component implements IEventTarget {
export class AnimationComponent extends Eventify(Component) {
/**
* 获取此动画组件的自有动画剪辑。
* 动画组件开始运行时会为每个自有动画剪辑创建动画状态。
Expand Down Expand Up @@ -180,8 +180,6 @@ export class AnimationComponent extends Component implements IEventTarget {
})
public playOnLoad = false;

public _callbackTable: ICallbackTable = createMap(true);

protected _crossFade = new CrossFade();

protected _nameToState: { [name: string]: AnimationState; } = createMap(true);
Expand Down Expand Up @@ -410,8 +408,8 @@ export class AnimationComponent extends Component implements IEventTarget {
* animation.on('play', this.onPlay, this);
* ```
*/
public on (type: string, callback: (type: string, state: AnimationState) => void, target?: Object) {
const ret = EventTarget.prototype.on.call(this, type, callback, target);
public on<TFunction extends Function> (type: EventType, callback: TFunction, thisArg?: any) {
const ret = super.on(type, callback, thisArg);
if (type === 'lastframe') {
for (const stateName of Object.keys(this._nameToState)) {
this._nameToState[stateName]!._lastframeEventOn = true;
Expand All @@ -434,33 +432,17 @@ export class AnimationComponent extends Component implements IEventTarget {
* animation.off('play', this.onPlay, this);
* ```
*/
public off (type: string, callback: Function, target?: Object) {
public off<TFunction extends Function> (type: EventType, callback?: TFunction, thisArg?: any) {
if (type === 'lastframe') {
const nameToState = this._nameToState;
for (const name of Object.keys(nameToState)) {
const state = nameToState[name]!;
state._lastframeEventOn = false;
}
}

EventTarget.prototype.off.call(this, type, callback, target);
super.off(type, callback, thisArg);
}

/**
* @en IEventTarget implementations, they will be overwrote with the same implementation in EventTarget by applyMixins
* @zh IEventTarget 实现,它们将被 applyMixins 在 EventTarget 中用相同的实现覆盖。
*/
public targetOff (keyOrTarget?: string | Object | undefined): void {}
public once (type: string, callback: Function, target?: Object | undefined): Function | undefined {
return;
}
public dispatchEvent (event: Event): void {}
public hasEventListener (key: string, callback?: Function | undefined, target?: Object | undefined): boolean {
return false;
}
public removeAll (keyOrTarget?: string | Object | undefined): void {}
public emit (key: string, ...args: any[]): void {}

protected _createState (clip: AnimationClip, name?: string) {
return new AnimationState(clip, name);
}
Expand Down Expand Up @@ -503,13 +485,6 @@ export class AnimationComponent extends Component implements IEventTarget {
}
}

// for restore on and off
const {on, off} = AnimationComponent.prototype;
applyMixins(AnimationComponent, [CallbacksInvoker, EventTarget]);
// restore
AnimationComponent.prototype.on = on;
AnimationComponent.prototype.off = off;

cc.AnimationComponent = AnimationComponent;

function equalClips (clip1: AnimationClip | null, clip2: AnimationClip | null) {
Expand Down
37 changes: 2 additions & 35 deletions cocos/core/assets/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,7 @@
*/

import { ccclass, property, string } from '../data/class-decorator';
import { Event } from '../event';
import { CallbacksInvoker } from '../event/callbacks-invoker';
import { EventTarget } from '../event/event-target';
import { applyMixins, IEventTarget } from '../event/event-target-factory';
import { createMap } from '../utils/js-typed';
import { Eventify } from '../event';
import { RawAsset } from './raw-asset';
import { Node } from '../scene-graph';

Expand All @@ -59,7 +55,7 @@ import { Node } from '../scene-graph';
* @extends RawAsset
*/
@ccclass('cc.Asset')
export class Asset extends RawAsset implements IEventTarget {
export class Asset extends Eventify(RawAsset) {

/**
* @en Indicates whether its dependent raw assets can support deferred load if the owner scene (or prefab) is marked as `asyncLoadAssets`.
Expand Down Expand Up @@ -111,33 +107,6 @@ export class Asset extends RawAsset implements IEventTarget {

private _file: any = null;

constructor (...args: ConstructorParameters<typeof RawAsset>) {
super(...args);
}

/**
* @en
* IEventTarget implementations, they will be overwrote with the same implementation in EventTarget by applyMixins
* @zh
* IEventTarget 实现,它们将被 applyMixins 在 EventTarget 中用相同的实现覆盖
*/
// tslint:disable-next-line: member-ordering
public _callbackTable = createMap(true);
public on (type: string, callback: Function, target?: Object | undefined): Function | undefined {
return;
}
public off (type: string, callback?: Function | undefined, target?: Object | undefined): void {}
public targetOff (keyOrTarget?: string | Object | undefined): void {}
public once (type: string, callback: Function, target?: Object | undefined): Function | undefined {
return;
}
public dispatchEvent (event: Event): void {}
public hasEventListener (key: string, callback?: Function | undefined, target?: Object | undefined): boolean {
return false;
}
public removeAll (keyOrTarget?: string | Object | undefined): void {}
public emit (key: string, ...args: any[]): void {}

/**
* @en
* Returns the url of this asset's native object, if none it will returns an empty string.
Expand Down Expand Up @@ -264,8 +233,6 @@ export class Asset extends RawAsset implements IEventTarget {
public createNode? (callback: CreateNodeCallback): void;
}

applyMixins(Asset, [CallbacksInvoker, EventTarget]);

/**
* @param error - null or the error info
* @param node - the created node or null
Expand Down
2 changes: 1 addition & 1 deletion cocos/core/assets/texture-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export type LoadImageCallback<T> = (
* @param target 回调函数的 `this` 参数。
* @returns 图像资源,返回时可能还未完成加载;加载完成或失败时会调用回调函数。
*/
export function loadImage<T> (url: string, callback?: LoadImageCallback<T>, target?: T) {
export function loadImage<T extends object> (url: string, callback?: LoadImageCallback<T>, target?: T) {
assertID(!!url, 3103);

let imageAsset = loader.getRes<ImageAsset>(url);
Expand Down
167 changes: 5 additions & 162 deletions cocos/core/event/event-target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,170 +28,13 @@
* @category event
*/

import * as js from '../utils/js';
import { CallbacksInvoker } from './callbacks-invoker';
import { Eventify } from './eventify';

const fastRemove = js.array.fastRemove;
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
class Empty {};

export interface ITargetImpl extends Object {
__eventTargets?: Object[];
node?: ITargetImpl;
}
export const EventTarget = Eventify(Empty);

/**
* !#en
* EventTarget is an object to which an event is dispatched when something has occurred.
* Entity are the most common event targets, but other objects can be event targets too.
*
* Event targets are an important part of the Fireball event model.
* The event target serves as the focal point for how events flow through the scene graph.
* When an event such as a mouse click or a keypress occurs, Fireball dispatches an event object
* into the event flow from the root of the hierarchy. The event object then makes its way through
* the scene graph until it reaches the event target, at which point it begins its return trip through
* the scene graph. This round-trip journey to the event target is conceptually divided into three phases:
* - The capture phase comprises the journey from the root to the last node before the event target's node
* - The target phase comprises only the event target node
* - The bubbling phase comprises any subsequent nodes encountered on the return trip to the root of the tree
* See also: http://www.w3.org/TR/DOM-Level-3-Events/#event-flow
*
* Event targets can implement the following methods:
* - _getCapturingTargets
* - _getBubblingTargets
*
* If a class cannot extend from EventTarget, it can consider implements IEventTarget interface.
*
* !#zh
* 事件目标是具有注册监听器、派发事件能力的类,Node 是最常见的事件目标,
* 但是其他类也可以继承自事件目标以获得管理监听器和派发事件的能力。
* 如果无法继承自 EventTarget,也可以考虑自实现 IEventTarget
*/
export class EventTarget extends CallbacksInvoker {
/**
* @en
* Register an callback of a specific event type on the EventTarget.
* This type of event should be triggered via `emit`.
* @zh
* 注册事件目标的特定事件类型回调。这种类型的事件应该被 `emit` 触发。
*
* @param type - A string representing the event type to listen for.
* @param callback - The callback that will be invoked when the event is dispatched.
* The callback is ignored if it is a duplicate (the callbacks are unique).
* @param target - The target (this object) to invoke the callback, can be null
* @return - Just returns the incoming callback so you can save the anonymous function easier.
* @example
* eventTarget.on('fire', function () {
* cc.log("fire in the hole");
* }, node);
*/
public on (type: string, callback: Function, target?: Object) {
if (!callback) {
cc.errorID(6800);
return;
}

if (!this.hasEventListener(type, callback, target)) {
super.on(type, callback, target);

const targetImpl = target as ITargetImpl;
if (target) {
if (targetImpl.__eventTargets) {
targetImpl.__eventTargets.push(this);
} else if (targetImpl.node && targetImpl.node.__eventTargets) {
targetImpl.node.__eventTargets.push(this);
}
}
}
return callback;
}

/**
* @en
* Removes the listeners previously registered with the same type, callback, target and or useCapture,
* if only type is passed as parameter, all listeners registered with that type will be removed.
* @zh
* 删除之前用同类型,回调,目标或 useCapture 注册的事件监听器,如果只传递 type,将会删除 type 类型的所有事件监听器。
*
* @param type - A string representing the event type being removed.
* @param callback - The callback to remove.
* @param target - The target (this object) to invoke the callback, if it's not given, only callback without target will be removed
* @example
* // register fire eventListener
* var callback = eventTarget.on('fire', function () {
* cc.log("fire in the hole");
* }, target);
* // remove fire event listener
* eventTarget.off('fire', callback, target);
* // remove all fire event listeners
* eventTarget.off('fire');
*/
public off (type: string, callback?: Function, target?: Object) {
if (!callback) {
this.removeAll(type);
}
else {
super.off(type, callback, target);

const targetImpl = target as ITargetImpl;
if (target) {
if (targetImpl.__eventTargets) {
fastRemove(targetImpl.__eventTargets, this);
} else if (targetImpl.node && targetImpl.node.__eventTargets) {
fastRemove(targetImpl.node.__eventTargets, this);
}
}
}
}

/**
* @en Removes all callbacks previously registered with the same target (passed as parameter).
* This is not for removing all listeners in the current event target,
* and this is not for removing all listeners the target parameter have registered.
* It's only for removing all listeners (callback and target couple) registered on the current event target by the target parameter.
* @zh 在当前 EventTarget 上删除指定目标(target 参数)注册的所有事件监听器。
* 这个函数无法删除当前 EventTarget 的所有事件监听器,也无法删除 target 参数所注册的所有事件监听器。
* 这个函数只能删除 target 参数在当前 EventTarget 上注册的所有事件监听器。
* @param target - The target to be searched for all related listeners
*/
public targetOff (keyOrTarget?: string | Object) {
this.removeAll(keyOrTarget);
}

/**
* @en
* Register an callback of a specific event type on the EventTarget,
* the callback will remove itself after the first time it is triggered.
* @zh
* 注册事件目标的特定事件类型回调,回调会在第一时间被触发后删除自身。
*
* @param type - A string representing the event type to listen for.
* @param callback - The callback that will be invoked when the event is dispatched.
* The callback is ignored if it is a duplicate (the callbacks are unique).
* @param target - The target (this object) to invoke the callback, can be null
* @example
* eventTarget.once('fire', function () {
* cc.log("this is the callback and will be invoked only once");
* }, node);
*/
public once (type: string, callback: Function, target?: Object) {
if (!callback) {
cc.errorID(6800);
return;
}

if (!this.hasEventListener(type, callback, target)) {
super.on(type, callback, target, true);

const targetImpl = target as ITargetImpl;
if (target) {
if (targetImpl.__eventTargets) {
targetImpl.__eventTargets.push(this);
} else if (targetImpl.node && targetImpl.node.__eventTargets) {
targetImpl.node.__eventTargets.push(this);
}
}
}
return callback;
}
}
export type EventTarget = InstanceType<typeof EventTarget>;

cc.EventTarget = EventTarget;
Loading

0 comments on commit ac34060

Please sign in to comment.