Bläddra i källkod

事件系统架构搭建

bill 1 år sedan
förälder
incheckning
e78f5d0dae

+ 1 - 30
src/board/core/base/factory.ts

@@ -2,40 +2,11 @@ import { getChangePart } from "../../shared";
 import {
   Entity,
   EntityShape,
-  EntityTransmit,
   EntityTree,
   EntityType,
   EntityTypeInstance,
 } from "./entity";
 
-const getEntityTransmitProps = (parent: Entity): EntityTransmit => {
-  return {
-    root: parent.root,
-  };
-};
-
-export const entityFactoryAfter = <
-  P extends Entity,
-  C extends Entity<any, EntityShape, EntityTree<P, C>>
->(
-  entity: C,
-  parent?: P
-) => {
-  if (parent) {
-    const transmit = getEntityTransmitProps(parent);
-    for (const key in transmit) {
-      entity[key] = parent[key];
-    }
-    entity.setParent(parent);
-  }
-
-  entity.init();
-  entity.mount();
-  if (parent?.isMounted) {
-    entity.mounted();
-  }
-};
-
 export const entityFactory = <
   P extends Entity,
   T,
@@ -55,7 +26,7 @@ export const entityFactory = <
   }) as EntityTypeInstance<C>;
 
   extra && extra(entity);
-  entityFactoryAfter(entity as any, parent);
+  parent.addChild(entity);
   created && created(entity);
   return entity;
 };

+ 12 - 13
src/board/core/base/entity-group.ts

@@ -6,28 +6,27 @@ import {
   EntityTree,
   EntityType,
 } from "./entity";
-import { IncEntitysFactory, incEntitysFactoryGenerate } from "./factory";
+import { IncEntitysFactory, incEntitysFactoryGenerate } from "./entity-factory";
 
 type Data<T extends EntityType> = T extends EntityType<infer D> ? D : never;
-type NT<T extends EntityType> = EntityType<
-  Data<T>,
-  EntityShape,
-  EntityTree<EntityGroup<T>, InstanceType<T>>
->;
 
-export class EntityGroup<T extends EntityType> extends Entity<
+export class EntityGroup<
+  T extends EntityType<
+    any,
+    EntityShape,
+    EntityTree<EntityGroup<T>, InstanceType<T>>
+  >
+> extends Entity<
   Data<T>[],
   Group,
   EntityTree<EntityGroup<T>, InstanceType<T>>
 > {
-  private incFactory: IncEntitysFactory<EntityGroup<T>, Data<T>, NT<T>>;
+  private incFactory: IncEntitysFactory<EntityGroup<T>, Data<T>, T>;
+  private;
 
-  constructor(props: EntityProps<Data<T>[]> & { type: NT<T> }) {
+  constructor({ type, ...props }: EntityProps<Data<T>[]> & { type: T }) {
     super(props);
-    this.incFactory = incEntitysFactoryGenerate(
-      props.type,
-      this as EntityGroup<T>
-    );
+    this.incFactory = incEntitysFactoryGenerate(type, this as EntityGroup<T>);
   }
 
   diffRedraw() {

+ 1 - 1
src/board/core/base/entity-map.ts

@@ -7,7 +7,7 @@ import {
   EntityType,
   EntityTypeInstance,
 } from "./entity";
-import { SingleEntityFactory, singleEntityFactory } from "./factory";
+import { SingleEntityFactory, singleEntityFactory } from "./entity-factory";
 
 export type EntityMapTypes<T> = {
   [key in keyof T]: EntityType;

+ 20 - 48
src/board/core/base/entity-root.ts

@@ -1,13 +1,9 @@
 import { Layer } from "konva/lib/Layer";
 import { Entity, EntityEvent, EntityTree } from "./entity";
-import {
-  EntityMap,
-  EntityMapData,
-  EntityMapTypes,
-  NEntityMapTypes,
-} from "./entity-map";
 import { Stage } from "konva/lib/Stage";
-import { entityFactoryAfter } from "./factory";
+import { entityInit, entityMount } from "./entity-util";
+import { injectMouseEvent } from "../event";
+import { Emitter } from "../mitt";
 
 export type RootEvent = EntityEvent & {
   dataChange: void;
@@ -16,68 +12,44 @@ export type RootEvent = EntityEvent & {
   viewChange: void;
 };
 
-export class Root<
-  Types extends EntityMapTypes<any> = EntityMapTypes<any>
-> extends Entity<
-  EntityMapData<Types>,
+export class Root<T extends Entity = any> extends Entity<
+  null,
   Layer,
-  EntityTree<never, EntityMap<Types>>
+  EntityTree<never, T, Root<Entity>>
 > {
-  private entityMap: EntityMap<Types, Root<Types>>;
   container?: HTMLDivElement;
   stage: Stage;
   tempLayer: Layer;
+  bus: Emitter<RootEvent>;
 
-  constructor(types: NEntityMapTypes<Types>) {
+  constructor() {
     super({
       name: "container",
-      attrib: {},
+      attrib: null,
     });
-    this.entityMap = new EntityMap({
-      types,
-      attrib: {},
-    }) as EntityMap<Types, Root<Types>>;
+    entityInit(this);
+    injectMouseEvent(this, this.stage);
   }
 
-  setContainer(dom: HTMLDivElement) {
-    const needUpdate = dom !== this.container && this.isMounted;
-    this.container = dom;
-    if (needUpdate) {
-      this.mount();
-    }
+  initShape(): Layer {
+    return new Layer();
   }
 
-  mount(): void {
-    if (!this.container) throw "mount 需要 container";
-    if (!this.stage) {
+  mount(container: HTMLDivElement): void {
+    if (container === this.container && this.isMounted) return;
+    if (!container) throw "mount 需要 container";
+    this.container = container;
+
+    if (!this.isMounted) {
       this.stage = new Stage({
         container: this.container,
         width: this.container.offsetWidth,
         height: this.container.offsetHeight,
       });
       this.stage.add(this.shape);
-      this.mounted();
+      entityMount(this);
     } else {
       this.stage.setContainer(this.container);
     }
   }
-
-  setAttrib(newAttrib: Partial<EntityMapData<Types>>): void {
-    this.entityMap.setAttrib(newAttrib);
-  }
-
-  diffRedraw(): void {
-    this.entityMap.diffRedraw();
-  }
-
-  initShape(): Layer {
-    return new Layer();
-  }
-
-  geta() {}
-
-  init(this: Root<Types>): void {
-    super.init();
-    entityFactoryAfter(this.entityMap, this);
-  }
 }

+ 194 - 0
src/board/core/base/entity-util.ts

@@ -0,0 +1,194 @@
+import { Layer } from "konva/lib/Layer";
+import { mergeFuns } from "../../shared";
+import { Entity, EntityShape, EntityTransmit } from "./entity";
+
+export const traversEntityTree = (
+  entity: Entity,
+  call: (entity: Entity, inverse: boolean) => void | "interrupt",
+  inverse: boolean | "all" = false
+) => {
+  if (!inverse || inverse === "all") {
+    if (call(entity, false) === "interrupt") return;
+  }
+
+  const eqed: Entity[] = [];
+  while (true) {
+    const child = entity.children.find((child) => eqed.includes(child));
+    if (!child) {
+      traversEntityTree(child, call, inverse);
+    } else {
+      break;
+    }
+  }
+
+  if (inverse || inverse === "all") {
+    if (call(entity, true) === "interrupt") return;
+  }
+};
+
+export const bubbleTraversEntityTree = (
+  entity: Entity,
+  call: (entity: Entity, inverse: boolean) => void | "interrupt",
+  inverse: boolean | "all" = false
+) => {
+  if (!inverse || inverse === "all") {
+    if (call(entity, false) === "interrupt") return;
+  }
+
+  bubbleTraversEntityTree(entity.parent, call, inverse);
+
+  if (inverse || inverse === "all") {
+    if (call(entity, true) === "interrupt") return;
+  }
+};
+
+export const findEntity = <T extends Entity>(
+  entity: Entity,
+  name: string
+): T => {
+  let find: T;
+  traversEntityTree(entity, (t) => {
+    if (t.name === name) {
+      find = t as T;
+      return "interrupt";
+    }
+  });
+  return find;
+};
+
+export const findEntityByShape = <T extends Entity>(
+  entity: Entity,
+  shape: EntityShape
+) => {
+  let find: T;
+  traversEntityTree(entity, (child) => {
+    if (child.shape === shape) {
+      find = entity as T;
+      return "interrupt";
+    }
+  });
+  return find;
+};
+
+export const getEntityNdx = (entity: Entity) => {
+  const parent = entity.parent;
+  if (!parent) return null;
+
+  const zIndex = entity.getZIndex();
+  const level = parent.children;
+  for (let i = level.length - 1; i >= 0; i--) {
+    if (level[i] !== this && level[i].getZIndex() <= zIndex) {
+      return i;
+    }
+  }
+  return -1;
+};
+
+export const summarizeEntity = (entity: Entity) => {
+  if (!entity.isMounted) return;
+
+  const packNdx = getEntityNdx(entity);
+  if (packNdx === null) return;
+
+  const packChild = entity.children;
+  const oldNdx = packChild.indexOf(this);
+
+  if (oldNdx !== packNdx + 1) {
+    let rep = entity;
+    for (let i = packNdx + 1; i < packChild.length; i++) {
+      const temp = packChild[i];
+      packChild[i] = rep;
+      rep = temp;
+    }
+  }
+
+  const parentShape = entity.getTeleport();
+  const levelShapes = parentShape.children;
+  const shapeNdx =
+    packNdx === -1 ? 0 : levelShapes.indexOf(packChild[packNdx].getShape()) + 1;
+  const oldShapeNdx = levelShapes.indexOf(entity.shape);
+
+  if (oldShapeNdx !== shapeNdx) {
+    if (shapeNdx !== 0) {
+      let repShape = entity.shape;
+      for (let i = shapeNdx; i < levelShapes.length; i++) {
+        const temp = levelShapes[i];
+        parentShape.add(repShape);
+        repShape.zIndex(i);
+        repShape = temp;
+      }
+    } else {
+      entity.shape.zIndex(0);
+    }
+  }
+};
+
+export const entityInit = <T extends Entity>(entity: T) => {
+  entity.bus.emit("createBefore");
+  entity.shape = entity.initShape();
+  entity.shape.id(entity.name);
+  entity.bus.emit("created");
+
+  let releases = entity.needReleases();
+  releases = Array.isArray(releases) ? releases : [releases];
+  if (releases.length) {
+    entity.bus.on("destroyBefore", () => {
+      mergeFuns(releases)();
+    });
+  }
+
+  const parentShape = (entity.getTeleport() || entity.parent.shape) as Layer;
+  parentShape.add(entity.shape);
+};
+
+export const entityMount = <T extends Entity>(entity: T) => {
+  traversEntityTree(entity, () => {
+    entity.bus.emit("mountBefore");
+    entity.diffRedraw();
+    summarizeEntity(entity);
+  });
+
+  traversEntityTree(
+    entity,
+    () => {
+      entity.isMounted = true;
+      entity.bus.emit("mounted");
+    },
+    true
+  );
+};
+
+export const setEntityParent = <T extends Entity>(
+  parent: T["parent"] | null,
+  entity: T
+) => {
+  const ndx = entity.parent.children.indexOf(this);
+  ~ndx && entity.parent.children.splice(ndx, 1);
+  entity.parent = parent;
+  parent && entity.parent.children.push(this);
+};
+
+const getEntityTransmitProps = (parent: Entity): EntityTransmit => {
+  return {
+    root: parent.root,
+  };
+};
+
+export const mountEntityTree = <T extends Entity>(
+  parent: T["parent"] | null,
+  entity: T
+) => {
+  if (parent) {
+    const transmit = getEntityTransmitProps(parent);
+    for (const key in transmit) {
+      entity[key] = parent[key];
+    }
+    setEntityParent(parent, entity);
+  }
+
+  entityInit(entity);
+  if (parent?.isMounted) {
+    entityMount(entity);
+  }
+  return entity;
+};

+ 19 - 45
src/board/core/base/entity.ts

@@ -4,9 +4,15 @@ import { EntityMouseStatus } from "../event";
 import mitt from "mitt";
 import { Root } from "./entity-root";
 import { Shape } from "konva/lib/Shape";
-import { findEntity, summarizeEntity, traversEntityTree } from "./util";
-import { mergeFuns } from "../../shared";
+import {
+  findEntity,
+  mountEntityTree,
+  setEntityParent,
+  summarizeEntity,
+  traversEntityTree,
+} from "./entity-util";
 import { Stage } from "konva/lib/Stage";
+import { Emitter } from "../mitt";
 import { DEV } from "../../env";
 
 export type EntityTransmit = {
@@ -30,7 +36,7 @@ export type EntityEvent = {
   destroyBefore: void;
   destroyed: void;
   updateAttrib: void;
-  updateMouseStatus: EntityMouseStatus;
+  mouseStatus: EntityMouseStatus;
 };
 
 export type EntityType<
@@ -72,11 +78,12 @@ export class Entity<
   private key: string;
   private teleport: Group | Layer;
 
+  contenteditable: boolean = false;
   attrib: T;
   shape: S;
   name: string;
   props: EntityProps<T>;
-  bus = mitt<EntityEvent>();
+  bus = mitt() as Emitter<EntityEvent>;
 
   // tree
   root: TR["root"];
@@ -120,60 +127,27 @@ export class Entity<
       throw "layer 只能挂载到 Stage";
     }
     this.teleport = tel;
+    summarizeEntity(this);
   }
 
   setAttrib(newAttrib: Partial<T>) {
     const attribUpdated = this.attrib !== newAttrib;
     this.attrib = newAttrib as T;
-    this.diffRedraw();
+    this.isMounted && this.diffRedraw();
     attribUpdated && this.bus.emit("updateAttrib");
   }
 
-  setParent(parent: TR["parent"] | null) {
-    const ndx = this.parent.children.indexOf(this);
-    ~ndx && this.parent.children.splice(ndx, 1);
-    this.parent = parent;
-
-    parent && this.parent.children.push(this);
-  }
-
-  needReleases(): Array<() => {}> {
+  needReleases(): (() => void) | Array<() => void> {
     return [];
   }
 
-  init() {
-    this.bus.emit("createBefore");
-    this.shape = this.initShape();
-    this.shape.id(this.name);
-    this.bus.emit("created");
-
-    const releases = this.needReleases();
-    if (releases.length) {
-      this.bus.on("destroyBefore", () => {
-        mergeFuns(releases)();
-      });
+  addChild(entity: TR["children"] | TR["children"][0]) {
+    const entitys = Array.isArray(entity) ? entity : [entity];
+    for (const entity of entitys) {
+      mountEntityTree(entity, this);
     }
   }
-
-  mount() {
-    this.bus.emit("mountBefore");
-    const parentShape = (this.teleport || this.parent.shape) as Layer;
-    parentShape.add(this.shape);
-    this.setZIndex(this.zIndex);
-  }
-
   isMounted = false;
-  mounted() {
-    traversEntityTree(
-      this,
-      () => {
-        this.diffRedraw();
-        this.isMounted = true;
-        this.bus.emit("mounted");
-      },
-      true
-    );
-  }
 
   getZIndex() {
     return this.zIndex;
@@ -193,7 +167,7 @@ export class Entity<
     while (this.children.length) {
       this.children[0].destory();
     }
-    this.setParent(null);
+    setEntityParent(null, this);
 
     if (DEV) {
       console.log(this.name, "destory");

+ 0 - 82
src/board/core/base/util.ts

@@ -1,82 +0,0 @@
-import { Entity } from "./entity";
-
-export const traversEntityTree = (
-  entity: Entity,
-  call: (entity: Entity, inverse: boolean) => void | "interrupt",
-  inverse: boolean | "all" = false
-) => {
-  if (!inverse || inverse === "all") {
-    if (call(entity, false) === "interrupt") return;
-  }
-  for (const child of entity.children) {
-    traversEntityTree(child, call, inverse);
-  }
-  if (inverse || inverse === "all") {
-    if (call(entity, true) === "interrupt") return;
-  }
-};
-
-export const getEntityNdx = (entity: Entity) => {
-  const parent = entity.parent;
-  if (!parent) return null;
-
-  const zIndex = entity.getZIndex();
-  const level = parent.children;
-  for (let i = level.length - 1; i >= 0; i--) {
-    if (level[i] !== this && level[i].getZIndex() <= zIndex) {
-      return i;
-    }
-  }
-  return -1;
-};
-
-export const summarizeEntity = (entity: Entity) => {
-  const packNdx = getEntityNdx(entity);
-  if (packNdx === null) return;
-
-  const packChild = entity.children;
-  const oldNdx = packChild.indexOf(this);
-
-  if (oldNdx !== packNdx + 1) {
-    let rep = entity;
-    for (let i = packNdx + 1; i < packChild.length; i++) {
-      const temp = packChild[i];
-      packChild[i] = rep;
-      rep = temp;
-    }
-  }
-
-  const parentShape = entity.getTeleport();
-  const levelShapes = parentShape.children;
-  const shapeNdx =
-    packNdx === -1 ? 0 : levelShapes.indexOf(packChild[packNdx].getShape()) + 1;
-  const oldShapeNdx = levelShapes.indexOf(entity.shape);
-
-  if (oldShapeNdx !== shapeNdx) {
-    if (shapeNdx !== 0) {
-      let repShape = entity.shape;
-      for (let i = shapeNdx; i < levelShapes.length; i++) {
-        const temp = levelShapes[i];
-        parentShape.add(repShape);
-        repShape.zIndex(i);
-        repShape = temp;
-      }
-    } else {
-      entity.shape.zIndex(0);
-    }
-  }
-};
-
-export const findEntity = <T extends Entity>(
-  entity: Entity,
-  name: string
-): T => {
-  let find: T;
-  traversEntityTree(entity, (t) => {
-    if (t.name === name) {
-      find = t as T;
-      return "interrupt";
-    }
-  });
-  return find;
-};

+ 109 - 3
src/board/core/event/index.ts

@@ -1,7 +1,113 @@
+import {
+  bubbleTraversEntityTree,
+  findEntityByShape,
+  traversEntityTree,
+} from "../base/entity-util";
+import { KonvaEventListener, KonvaEventObject } from "konva/lib/Node";
+import { getChangePart } from "../../shared";
+import { Stage } from "konva/lib/Stage";
+import { Entity, EntityShape } from "../base/entity";
+
 export type EntityMouseStatus = {
-  draging: boolean;
+  drag: boolean;
   hover: boolean;
-  active: boolean;
   focus: boolean;
-  common: boolean;
+};
+
+export const onEntityEvent = (
+  entity: Entity,
+  namespace: string,
+  shape: EntityShape | Stage = entity.shape
+) => {
+  const events: string[] = [];
+  const use = (
+    name: string,
+    cb: KonvaEventListener<any, KonvaEventObject<any>>
+  ) => {
+    const eventName = `${name}.${namespace}`;
+    shape.on(eventName, cb);
+    events.push(eventName);
+  };
+  entity.bus.on("destroyBefore", () => shape.off(events.join(" ")));
+
+  return use;
+};
+
+export const injectMouseEvent = (root: Entity, stage: Stage) => {
+  const store = {
+    hovers: new Set<Entity>(),
+    focus: new Set<Entity>(),
+    drag: null as Entity,
+  };
+  const oldStore = {
+    hovers: [] as Entity[],
+    focus: [] as Entity[],
+    drag: null as Entity,
+  };
+  const use = onEntityEvent(root, "mouse-style", stage);
+
+  const emit = () => {
+    const hovers = [...store.hovers];
+    const focus = [...store.focus];
+    const hoverChange = getChangePart(hovers, oldStore.hovers);
+    const focusChange = getChangePart(focus, oldStore.hovers);
+    const changeEntitys = new Set<Entity>([
+      ...hoverChange.addPort,
+      ...hoverChange.delPort,
+      ...focusChange.addPort,
+      ...focusChange.delPort,
+    ]);
+    if (oldStore.drag !== store.drag) {
+      oldStore.drag && changeEntitys.add(oldStore.drag);
+      store.drag && changeEntitys.add(store.drag);
+    }
+
+    for (const entity of changeEntitys) {
+      const status: EntityMouseStatus = {
+        hover: hovers.includes(entity),
+        focus: focus.includes(entity),
+        drag: store.drag === entity,
+      };
+      entity.bus.emit("mouseStatus", status);
+    }
+
+    oldStore.drag = store.drag;
+    oldStore.hovers = hovers;
+    oldStore.focus = focus;
+  };
+
+  use(`mouseenter`, (ev) => {
+    const entity = findEntityByShape(root, ev.target);
+    bubbleTraversEntityTree(entity, (hit) => {
+      store.hovers.add(hit);
+    });
+    emit();
+  });
+
+  use(`mouseleave`, (ev) => {
+    const entity = findEntityByShape(root, ev.target);
+    traversEntityTree(entity, (hit) => {
+      store.hovers.delete(hit);
+    });
+    emit();
+  });
+
+  use(`click touchend`, (ev) => {
+    store.focus.clear();
+    const entity = findEntityByShape(root, ev.target);
+    bubbleTraversEntityTree(entity, (hit) => {
+      hit.contenteditable && store.focus.add(hit);
+    });
+    emit();
+  });
+
+  use(`dragstart`, (ev) => {
+    const hit = findEntityByShape(root, ev.target);
+    hit.contenteditable && (store.drag = hit);
+    emit();
+  });
+  use(`dragend`, () => {
+    store.drag = null;
+    emit();
+  });
 };

+ 152 - 0
src/board/core/event/spread.ts

@@ -0,0 +1,152 @@
+import { Entity, EntityShape } from "../base/entity";
+import { KonvaEventObject } from "konva/lib/Node";
+import {
+  bubbleTraversEntityTree,
+  findEntityByShape,
+} from "../base/entity-util";
+import { Stage } from "konva/lib/Stage";
+
+const getEventArgs = (key: string) => {
+  const [name, ...args] = key.split(".");
+  const useCapture = args.includes("capture");
+  return [name, useCapture] as const;
+};
+
+// 将konva的时间转发到entity
+const entityTreeDistributor = (
+  root: Entity,
+  shape: EntityShape | Stage = root.shape
+) => {
+  const handlerMap: { [key in string]: Entity[] } = {};
+  const addedKeys: string[] = [];
+
+  const addEvent = (key: string) => {
+    const [name] = getEventArgs(key);
+    if (addedKeys.includes(name)) return;
+    shape.on(name, (ev: KonvaEventObject<any>) => {
+      const self = findEntityByShape(root, ev.target);
+      const paths: Entity[] = [];
+      bubbleTraversEntityTree(self, (t) => {
+        paths.push(t);
+      });
+
+      const bubbles: Entity[] = [];
+      const captures: Entity[] = [];
+      for (const key in handlerMap) {
+        const [cname, useCapture] = getEventArgs(key);
+        if (name === cname) {
+          if (useCapture) {
+            captures.push(...handlerMap[key]);
+          } else {
+            bubbles.push(...handlerMap[key]);
+          }
+        }
+      }
+
+      const bubblePaths = bubbles
+        .filter((entity) => paths.includes(entity))
+        .sort((a, b) => paths.indexOf(a) - paths.indexOf(b));
+      for (const entity of bubblePaths) {
+        entity.bus.emit(name as any, ev);
+        if (ev.cancelBubble) break;
+      }
+      ev.cancelBubble = false;
+
+      const capturePaths = captures
+        .filter((entity) => paths.includes(entity))
+        .sort((a, b) => paths.indexOf(b) - paths.indexOf(a));
+      for (const entity of capturePaths) {
+        entity.bus.emit((name + ".capture") as any, ev);
+        if (ev.cancelBubble) break;
+      }
+      ev.cancelBubble = false;
+    });
+    addedKeys.push(name);
+  };
+
+  const delEvent = (key: string) => {
+    const [name] = getEventArgs(key);
+    const ndx = addedKeys.indexOf(name);
+    if (!~ndx) return;
+    shape.off(name);
+    addedKeys.splice(ndx, 1);
+  };
+
+  return {
+    on: (entity: Entity, key: string) => {
+      if (!handlerMap[key]) {
+        handlerMap[key] = [];
+      }
+      handlerMap[key].push(entity);
+      addEvent(key);
+    },
+    off: (entity: Entity, key?: string) => {
+      const keys = key ? [key] : Object.keys(handlerMap);
+      const vals = key
+        ? entity[key]
+          ? [entity[key]]
+          : []
+        : Object.values(handlerMap);
+
+      const delKeys: string[] = [];
+      for (let i = 0; i < vals.length; i++) {
+        const ckey = keys[i];
+        const items = vals[i];
+
+        let j = 0;
+        for (; j < items.length; j++) {
+          const centity = items[j];
+          if (entity === centity) {
+            items.splice(j--, 1);
+          }
+        }
+        if (j === 0) {
+          delete handlerMap[ckey];
+          delKeys.push(ckey);
+        }
+      }
+      if (!delKeys.length) return;
+
+      const ckeys = Object.keys(handlerMap);
+      for (const delKey of delKeys) {
+        const delName = getEventArgs(delKey)[0];
+        if (!ckeys.some((ckey) => getEventArgs(ckey)[0] === delName)) {
+          delEvent(delKey);
+        }
+      }
+    },
+    exixtsEvents: addedKeys,
+    destory() {
+      while (addedKeys.length) {
+        delEvent(addedKeys[0]);
+      }
+    },
+  };
+};
+
+const distributors = new WeakMap<
+  Entity,
+  ReturnType<typeof entityTreeDistributor>
+>();
+const getDistributor = (entity: Entity) => {
+  let distributor = distributors.get(entity.root);
+  if (!distributor) {
+    distributor = entityTreeDistributor(entity.root);
+    distributors.set(entity.root, distributor);
+  }
+  return distributor;
+};
+
+const canHandlerEvents = [""];
+export const onEntity = (entity: Entity, key: string) => {
+  const distributor = getDistributor(entity);
+  distributor.on(entity, key);
+  entity.bus.on("mountBefore", () => {
+    distributor.off(entity, key);
+  });
+};
+
+export const offEntity = (entity: Entity, key?: string) => {
+  const distributor = getDistributor(entity);
+  distributor.off(entity, key);
+};

+ 13 - 0
src/board/core/mitt.d.ts

@@ -0,0 +1,13 @@
+import { EventType, Handler } from "mitt";
+
+export interface Emitter<Events extends Record<EventType, unknown>> {
+  on<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;
+  off<Key extends keyof Events>(
+    type: Key,
+    handler?: Handler<Events[Key]>
+  ): void;
+  emit<Key extends keyof Events>(type: Key, event: Events[Key]): void;
+  emit<Key extends keyof Events>(
+    type: undefined extends Events[Key] ? Key : never
+  ): void;
+}