cocosCreator’s dispatchEvent event distribution

Version: 3.8.0

Language: TypeScript

Environment: Mac

Node event dispatch

cocosCreator supports using Node nodes for event dispatch (dispatchEvent). The event dispatch system is implemented in accordance with the Web’s event bubbling and capturing standards.

Event dispatch is mainly passed to the parent node gradually through bubbling.

bubble-event

After distribution, it will go through the following stages:

  • Capture: The event is passed from the scene root node to the child nodes step by step until it reaches the target node or the event delivery is interrupted in the response function of a node
  • Target: The event is triggered on the target node
  • Bubbling: The event is bubbled from the target node to the parent node step by step until it reaches the root node or the event delivery is interrupted in the response function of a node

The main interfaces implemented are in the base class BaseNode of Node, mainly including:

export class BaseNode extends CCObject implements ISchedulable {<!-- -->
  //Register a callback function of the specified type on the node. You can also set the target to the this object used to bind the response function.
  on(type: string | __private.cocos_core_scene_graph_node_event_NodeEventType, callback: __private.AnyFunction, target?: unknown, useCapture?: any): void;
  // Delete previously registered callbacks with the same type, callback, target or useCapture
  off(type: string, callback?: __private.AnyFunction, target?: unknown, useCapture?: any): void;
  // Register a specific event type callback of the node. The callback will delete itself after being triggered for the first time.
  once(type: string, callback: __private.AnyFunction, target?: unknown, useCapture?: any): void;
  //Send custom events through event names, supporting the passing of up to 5 parameters
  emit(type: string, arg0?: any, arg1?: any, arg2?: any, arg3?: any, arg4?: any): void;
  // Distribute events to the event stream
  dispatchEvent(event: Event): void;
  // Check if the event target object has a callback registered for a specific type of event
  hasEventListener(type: string, callback?: __private.AnyFunction, target?: unknown): any;
  // Remove all registered events on the target
  targetOff(target: string | unknown): void;
}

Using dispatchEvent for event distribution requires the support of Event object, which is the base class of all event objects. Its main definitions are:

export class Event {<!-- -->
  static NO_TYPE: string; // Event without type
  static TOUCH: string; // Touch event type
  static MOUSE: string; //Mouse event type
  static KEYBOARD: string; //Keyboard event type
  static ACCELERATION: string; // Accelerator event type
  
  static NONE: number; // The event phase has not been dispatched yet
  static CAPTURING_PHASE: number; // Capturing phase
  static AT_TARGET: number; // Target stage
  static BUBBLING_PHASE: number; // bubbling phase
  
  bubbles: boolean; // Whether the event bubbles
  target: any; // The target of the initial event trigger
  currentTarget: any; // current target
  
  // Event stage, mainly used to return NONE, CAPTURING_PHASE, AT_TARGET, BUBBLING_PHASE, etc.
  eventPhase: number;
  // Stop delivering the current event
  propagationStopped: boolean;
  // Immediately stop delivery of the current event, the event will not even be dispatched to the current connected target
  propagationImmediateStopped: boolean;
  
  // Check whether the event has stopped being delivered
  isStopped(): boolean;
  // Get the current target node
  getCurrentTarget(): any;
  // Get event type
  getType(): __private.cocos_input_types_event_enum_SystemEventTypeUnion;
}

Taking the above figure as an example, assuming that the event is dispatched from node c, and both nodes a and b receive event monitoring, the example can be written like this:

// common.ts
class MyEvent extends Event {<!-- -->
    constructor(name: string, bubbles?: boolean, detail?: any) {<!-- -->
        super(name, bubbles);
        this.detail = detail;
    }
    public detail: any = null; // Custom attributes
}

//c.ts
import {<!-- --> Event } from 'cc';

public demo() {<!-- -->
  this.node.dispatchEvent( new MyEvent('foobar', true, 'detail info') );
}

// b.ts
this.node.on('foobar', (event: MyEvent) => {<!-- -->
  //Set whether to stop delivering the current event. If it is true, a will no longer receive listening events.
  event.propagationStopped = true;
});

// a.ts
this.node.on('foobar', (event: MyEvent) => {<!-- -->
  //
});

The above content is transplanted from the official document: Node Event System, and is mainly used to pave the way for subsequent custom event distribution.

Custom event dispatch

Using bubbling dispatch of Node nodes, if there are too many component nodes, there may be problems of inflexibility and inefficiency.

The general principle of event distribution is:

  • Register events into an event table through dispatchEvent
  • Use addEventListener to detect whether the event exists in the event table according to the event type, and execute if it exists
  • Use removeEventListener to remove the event from the table according to the event type, if it exists, remove it

Therefore, a simple event management class can be encapsulated: EventManager, which roughly implements:

import {<!-- --> error, _decorator } from "cc";
const {<!-- --> ccclass } = _decorator;

@ccclass("EventManager")
export class EventManager {<!-- -->
  static handlers: {<!-- --> [name: string]: {<!-- --> handler: Function, target: any }[] };
  //Add listener (event type name, callback, target node)
  public static addEventListener(name: string, handler: Function, target?: any) {<!-- -->
    const objHandler = {<!-- -->handler: handler, target: target};
    if (this.handlers === undefined) {<!-- -->
      this.handlers = {<!-- -->};
    }
    let handlerList = this.handlers[name];
    if (!handlerList) {<!-- -->
      handlerList = [];
      this.handlers[name] = handlerList;
    }

    for (var i = 0; i < handlerList.length; i + + ) {<!-- -->
      if (!handlerList[i]) {<!-- -->
        handlerList[i] = objHandler;
        return i;
      }
    }

    handlerList.push(objHandler);

    return handlerList.length;
  };
  // Remove the listener (event type name, callback, target node)
  public static removeEventListener(name: string, handler: Function, target?: any) {<!-- -->
    const handlerList = this.handlers[name];
    if (!handlerList) {<!-- -->
      return;
    }

    for (let i = 0; i < handlerList.length; i + + ) {<!-- -->
      const oldObj = handlerList[i];
      if (oldObj.handler === handler & amp; & amp; (!target || target === oldObj.target)) {<!-- -->
        handlerList.splice(i, 1);
        break;
      }
    }
  };
  //Event distribution (event type name, custom parameters)
  public static dispatchEvent(name: string, ...args: any) {<!-- -->
    const handlerList = this.handlers[name];

    const params = [];
    let i;
    for (i = 1; i < arguments.length; i + + ) {<!-- -->
      params.push(arguments[i]);
    }

    if (!handlerList) {<!-- -->
      return;
    }

    for (i = 0; i < handlerList.length; i + + ) {<!-- -->
      const objHandler = handlerList[i];
      if (objHandler.handler) {<!-- -->
        objHandler.handler.apply(objHandler.target, args);
      }
    }
  };
};

Usage example:

// demoLayer.ts
import {<!-- --> EventManager } from '../EventManager';
export class demoLayer extends Component {<!-- -->
  protected onEnable(): void {<!-- -->
    EventManager.on("DEBUG_CUSTOM", this.customEvent, this);
  }

  protected onDisable(): void {<!-- -->
    EventManager.off("DEBUG_CUSTOM", this.customEvent, this);
  }
  
  private customEvent(param) {<!-- -->
    console.log(param);
  }
}

// gameManager.ts
EventManager.dispatchEvent("DEBUG_CUSTOM", 1);

The understanding may be wrong, please point it out; finally, I wish everyone a happy study and life!