Кажется, я придумал новую архитектуру ивентов и мне она нравится

от автора

Даже не знаю с чего начать, это моя первая статья и пишу я ее по причине того что мне не с кем обсудить ее содержимое. Для контекста добавлю, что я самоучка без работы.

Stateful Event Multiplexing Bus

Именно такое название мне дал чат гпт, когда я спросил его о моем подходе, и как он мне сообщил, то что я придумал, это уникально и (цитирую) «Годнота!». Но названия у всей этой истории нету, ибо я не силен в нейминге, но в коде она называется «MEctx». Можете предложить название, мб приживется…

Так кто же такой этот «MEctx»

Если описывать моими мыслями (а я не знаю теорию js), то получается следующее:

  • Общий хендлер событий — все ивенты обрабатываются в единственном обработчике

  • Количество ивентов — я подписываюсь только по ОДНОМУ разу на каждый тип ивента, и все они ведут в общий хендлер

  • строгое распространение ивентов — в общем хендлере хранится объект с ключом названия ивента, в котором лежат ивенты под необходимый режим работы

  • Режимы — в обработчике хранится переменная хранящая ключ по которому будут вызываться ивенты в их хранилищах

Плюсы данной архитектуры:

  • Единая точка входа ивентов — большой контроль + легкий дебаг

  • Режимы позволяют вызывать только требуемые в данный момент ивенты

  • Не вызывает ререндеры — в моей реализации общий хендлер хранится в глобальном скопе страницы (window), но можно спокойно перенести все в «useContext» и ничего не сломается

  • Обновление коллбеков — если нужно заменить ивент или убрать или добавить, то нужно просто обратиться в общий хендлер и сделать необходимую операцию с объектом по ключу названия ивента, это не вызовет ререндер

  • Минимальное взаимодействие с DOM деревом — так как в данной архитектуре мы 1 раз вешаем ивент и направляем его коллбеком в общий хендлер, то на первом рендере все махинации с деревом прекратятся

  • Возможность создать мидлвар для ивентов

Код

Внесу еще немного контекста, код был написал 11 месяцев назад и в данной реализации он заточен под взаимодействие с картой на базе «react-map-gl», но все можно спокойно переписать под любую задачу. Я не обязан дать вам готовый код, я всего лишь хочу показать вам такой подход.

import { Map, MapLibreEvent, MapMouseEvent } from "maplibre-gl"; import { MapCollection } from "react-map-gl/dist/esm/components/use-map";  // это строки префиксы названий слоев инструментов import {   IMAGE_PREFIX_GHOST,   PIN_PREFIX,   POLYGON_PREFIX,   ROUTE_PREFIX, } from "~/components/_store/geometry"; // это строка текущего выбранного инструмента карты import { MAP_TOOL } from "~/components/_store/project"; // это строка префикс названия слоев датасетов import { DATASET_PREFIX } from "../../../_store/datasets";  //   ____         ___     ____________ //  /\   '.      /   \   /\   ________\ //  \ \    '.   /     \  \ \  \_______/ //   \ \  \. './   /\  \  \ \  \_________      ________  ___   __    __ //    \ \  \'.    /\ \  \  \ \   ________\     | _____| _| |_  \ \  / / //     \ \  \.'._/  \ \  \  \ \  \_______/     | |      |_ _|   \ \/ / //      \ \  \'./    \ \  \  \ \  \_________   | |       | |     }  { //       \ \__\       \ \__\  \ \___________\  | |____   | |_   / /\ \ //        \/__/        \/__/   \/___________/  |______|  |___| /_/  \_\ // // //  MECtx // //  created:                4.05.24 //  successfully applied:   10.05.24 // //  example: // //  useEffect(() => { //    let handleClick = (str) => { //      console.log(str) //      if (ME.tool) { //        ME.tool = null //      } else { //        ME.tool = "pin" //      } //    } // //    ME.click.stock = (e) => handleClick("stock event")  // call callback only when ME.tool == null //    ME.click.pin = (e) => handleClick("pin event")      // call callback only when ME.tool == "pin" // //    return () => { //      ME.click.stock = () => {} //      ME.click.pin = () => {} //    } //  }, []) // //  ME can store a lot of callbacks, but always call only one  // MAP_TOOL это строки названия инструментов type eventsList = MAP_TOOL | "stock" | "cs";  type handlersList =   | "contextmenu"   | "click"   | "mousedown"   | "mouseup"   | "mousemove"   | "mouseenter"   | "mouseleave"   | "mouseout"   | "mouseover"   | "drag"   | "dragend"   | "dragstart"   | "move"   | "moveend"   | "movestart"   | "zoom"   | "zoomend"   | "zoomstart";  export enum METoolModes {   None = 0,   Point = 1,   Fill = 2,   Line = 3,   Between = 4, }  export type MapEvents = {   /**    * *DO NOT REDECLARE AFTER `inited` PROPERTY: `true`*    *    * shortcut for `click` and `contextmenu` events in `clickChecker` function    * @param e map event    * @param type event from `eventsList`    * @param rc `false` - LeftClick, `true` - RightClick    */   handleClick: (e: MapMouseEvent, type: eventsList, rc: boolean) => void;    /**    * handles `MapMouseEvent<>`    * @param e map event    * @param handler event from `eventsList`    */   handleEvent: (e: MapMouseEvent, handler: handlersList) => void;   /**    * handles `MapMouseEvent<MouseEvent | TouchEvent | undefined>`    * @param e map event    * @param handler event from `eventsList`    */   handleMTEvent: (     e: MapLibreEvent<MouseEvent | TouchEvent | undefined>,     handler: handlersList,   ) => void;   /**    * handles `MapMouseEvent<MouseEvent | TouchEvent | WheelEvent | undefined>`    * @param e map event    * @param handler event from `eventsList`    */   handleMTWEvent: (     e: MapLibreEvent<MouseEvent | TouchEvent | WheelEvent | undefined>,     handler: handlersList,   ) => void;    contextmenu: {     [key in eventsList]?: (e: MapMouseEvent) => void;   };    click: {     [key in eventsList]?: (e: MapMouseEvent) => void;   };    mousedown: {     [key in eventsList]?: (e: MapMouseEvent) => void;   };   mouseup: {     [key in eventsList]?: (e: MapMouseEvent) => void;   };   mousemove: {     [key in eventsList]?: (e: MapMouseEvent) => void;   };   mouseenter: {     [key in eventsList]?: (e: MapMouseEvent) => void;   };   mouseleave: {     [key in eventsList]?: (e: MapMouseEvent) => void;   };   mouseout: {     [key in eventsList]?: (e: MapMouseEvent) => void;   };   mouseover: {     [key in eventsList]?: (e: MapMouseEvent) => void;   };    drag: {     [key in eventsList]?: (e: MapLibreEvent<MouseEvent | TouchEvent | undefined>) => void;   };   dragend: {     [key in eventsList]?: (e: MapLibreEvent<MouseEvent | TouchEvent | undefined>) => void;   };   dragstart: {     [key in eventsList]?: (e: MapLibreEvent<MouseEvent | TouchEvent | undefined>) => void;   };    move: {     [key in eventsList]?: (       e: MapLibreEvent<MouseEvent | TouchEvent | WheelEvent | undefined>,     ) => void;   };   moveend: {     [key in eventsList]?: (       e: MapLibreEvent<MouseEvent | TouchEvent | WheelEvent | undefined>,     ) => void;   };   movestart: {     [key in eventsList]?: (       e: MapLibreEvent<MouseEvent | TouchEvent | WheelEvent | undefined>,     ) => void;   };    zoom: {     [key in eventsList]?: (       e: MapLibreEvent<MouseEvent | TouchEvent | WheelEvent | undefined>,     ) => void;   };   zoomend: {     [key in eventsList]?: (       e: MapLibreEvent<MouseEvent | TouchEvent | WheelEvent | undefined>,     ) => void;   };   zoomstart: {     [key in eventsList]?: (       e: MapLibreEvent<MouseEvent | TouchEvent | WheelEvent | undefined>,     ) => void;   };    /**    * property for selecting events    *    * if tool equals `null` => call `Stock` callbacks    *    * if tool equals `some_tool` => call `some_tool` callbacks    */   tool: MAP_TOOL | null;    /**    * RightClick - `true` value allows instrument to show Bbox    */   rc: boolean;    /**    *    */   toolMode: METoolModes;    map: {     current: Map & {       /**        * get real map object        */       getMap: () => Map;     };   };   init: (Map: MapCollection<Map>) => void;   inited: boolean; };  let runEvent = (e: any, handler: handlersList, ctx: MapEvents) => {   if (ctx.tool) {     if (ctx[handler][ctx.tool]) {       ctx[handler][ctx.tool]!(e);     }   } else {     if (!ctx.toolMode && ctx[handler]["stock"]) {       ctx[handler]["stock"]!(e);     }   } };  // пример мидлвара // функция для стоковых ивентов click и contextmenu let clickChecker = function (e: MapMouseEvent, rc: boolean, ctx: MapEvents) {   // получаем слои под кликом   let featuresUnderClick = ctx.map.current.queryRenderedFeatures(e.point);    if (featuresUnderClick.length) {     let layer = featuresUnderClick[0]?.layer;      let id = layer?.id.split(":")[0];      switch (id) {       case DATASET_PREFIX:         ctx.handleClick(e, "dataset_info", rc);         break;       case POLYGON_PREFIX:         ctx.handleClick(e, "polygon", rc);         break;       case ROUTE_PREFIX:         ctx.handleClick(e, "route", rc);         break;       case PIN_PREFIX:         ctx.handleClick(e, "pin", rc);         break;       case IMAGE_PREFIX_GHOST:         ctx.handleClick(e, "image", rc);         break;       default: {         // ctx.setTool(null);       }     }   } };  export const MEInitialGlobalObject: MapEvents = {   handleClick: function (e, type, rc) {     if (rc) {       if (this.contextmenu[type]) {         this.tool = type;         this.contextmenu[type](e);       }     } else {       if (this.click[type]) {         this.tool = type;         this.click[type](e);       }     }   },    handleEvent: function (e, handler) {     runEvent(e, handler, this);   },   handleMTEvent: function (e, handler) {     runEvent(e, handler, this);   },   handleMTWEvent: function (e, handler) {     runEvent(e, handler, this);   },    contextmenu: {},   click: {},    mousedown: {},   mouseup: {},   mousemove: {},   mouseenter: {},   mouseleave: {},   mouseout: {},   mouseover: {},    drag: {},   dragend: {},   dragstart: {},    move: {},   moveend: {},   movestart: {},    zoom: {},   zoomend: {},   zoomstart: {},    tool: null,   map: {     // @ts-ignore     current: null,   },   init: function (Map) {     this.map.current = Map.current;      if (!this.inited) {       this.inited = true;        this.click.stock = (e: MapMouseEvent) => {         clickChecker(e, false, this);       };       this.contextmenu.stock = (e: MapMouseEvent) => {         clickChecker(e, true, this);       };     }   },   inited: false, }; 

Регистрация ивентов на карте выглядит так:

onClick={(e) => ME.handleEvent(e, "click")} onContextMenu={(e) => ME.handleEvent(e, "contextmenu")} onMouseDown={(e) => ME.handleEvent(e, "mousedown")} onMouseUp={(e) => ME.handleEvent(e, "mouseup")} onMouseMove={(e) => {   ME.handleEvent(e, "mousemove");   if (ME.inited && !ME.toolMode && ME.mousemove.cs) {     ME.mousemove.cs(e);   } }} onMouseEnter={(e) => ME.handleEvent(e, "mouseenter")} onMouseLeave={(e) => ME.handleEvent(e, "mouseleave")} onMouseOut={(e) => ME.handleEvent(e, "mouseout")} onMouseOver={(e) => ME.handleEvent(e, "mouseover")} onDrag={(e) => ME.handleMTEvent(e, "drag")} onDragEnd={(e) => ME.handleMTEvent(e, "dragend")} onDragStart={(e) => ME.handleMTEvent(e, "dragstart")} onMove={(e) => ME.handleMTWEvent(e, "move")} onMoveEnd={(e) => ME.handleMTWEvent(e, "moveend")} onMoveStart={(e) => ME.handleMTWEvent(e, "movestart")} onZoom={(e) => {   ME.handleMTWEvent(e, "zoom");   setPopupMaxWidth(getMaxWidthFromZoom()); }} onZoomEnd={(e) => ME.handleMTWEvent(e, "zoomend")} onZoomStart={(e) => ME.handleMTWEvent(e, "zoomstart")}

Регистрация происходит 1 раз и больше мы не мучаем бедную карту.

Базовое использование выглядит так:

useEffect(() => {   // проверяем необходимость обновить коллбек   if (tool == "pin" && drawmode) {          // создаем простую функцию как и всегда     let handleClick = (e: MapMouseEvent) => {       //       // логика       //     };      // вешаем ивент     ME.click.pin = (e) => handleClick(e);      return () => {       // удаляем по необходимости       ME.click.pin = (e) => () => {};     };   } }, [...]);

То длинное полотно кода конечно по хорошему было бы разделить как в других статьях, но я приверженец простого копи-паст.

В общем, описываю жизненный цикл ивента в этой структуре:

  • На первом рендере — создаем обработчик, инициализируем «ME.init(Map)», и вешаем начальные ивенты там где нам необходимо, в моем случае на карте.

  • Вызов ивента — коллбеком вызываем общий хендлер и передаем оригинальный объект ивента

  • Анализ ивента — общий обработчик смотрит текущий режим работы, если ивент не поддерживается, игнорирует его, если ивент можно вызвать, но в текущем режиме нет такого слушателя, вызывается «stock» коллбек (это нечто вроде глобального коллбека, который вызывается только когда больше вызывать нечего)

  • Вызов мидлвара по необходимости

  • Вызов необходимого ивента

Получается так что мы можем создать сколько угодно каких угодно ивентов, и это не будет вызывать так же много нагрузки на браузер как простое вешание ивентов на все подряд так как ивенты в этой архитектуре — это просто функции в объекте.

Так же можно немного отредактировать код и сделать «режим» массивом строк, что позволит вызывать сразу несколько ивентов, хотя изначально браузер вызвал только один.

Наверное на этом все, надеюсь, я не придумал велосипед…


ссылка на оригинал статьи https://habr.com/ru/articles/910100/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *