Даже не знаю с чего начать, это моя первая статья и пишу я ее по причине того что мне не с кем обсудить ее содержимое. Для контекста добавлю, что я самоучка без работы.
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/
Добавить комментарий