Plate 富文本编辑器事件委托预算(Event Delegation Budget)实战指南
Plate 富文本编辑器事件委托预算Event Delegation Budget实战指南【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate导读本文围绕 plate 仓库性能规则文档 event-delegation-budget.md 展开讲解在块级元素、行、叶子节点等**重复单元repeated units**大量出现时如何通过事件委托Event Delegation与data-*命中路由来控制监听器数量避免每个单元各自绑定 pointer、hover、键盘、拖拽与选区处理器导致的性能退化。读完本文你将掌握一套可量化的委托预算模型每重复单元处理器数、目标队列活跃处理器总数、每次渲染热路径分配数等、20 处理器即触发告警的判定阈值以及 plate 源码中 document/window 级委托、data-block-id路由、条件式 resize 监听等真实落地手法可直接套用于长文档、表格、列表、虚拟化等高频场景的性能评审。适用范围什么时候启用本规则该规则并非所有编辑器代码都必须遵守而是针对以下特征场景启用大量重复单元各自挂载 pointer鼠标指针处理器大量重复单元各自挂载 hover悬停处理器大量重复单元各自挂载 keyboard键盘处理器大量重复单元各自挂载 drag拖拽处理器大量重复单元各自挂载 selection选区处理器。这里的重复单元概念与同目录下的 repeated-unit-budget.md 一脉相承包括 blocks块、lines行、leaves叶子、rows行/表格行、decorations装饰、shell islands、groups 等。当编辑器中同时存在成千上万个这样的单元如 1 万行文档、数百行表格时每个单元多挂一个处理器总量就会线性放大这正是本规则要扼制的增长点。核心规则委托到稳定父级用>onDragStart: ({ editor, event, plugin }) { const target event.target as HTMLElement; const dataTransfer (event as React.DragEvent).dataTransfer!; dataTransfer.effectAllowed move; dataTransfer.dropEffect move; const id target.dataset.blockId; if (!id) return; editor.setOption(plugin, draggingId, id); editor.setOption(plugin, isDragging, true); editor.setOption(plugin, _isOver, true); },这里没有任何按块注册的处理器块的身份完全来自 DOM 上的data-block-id属性而在handleDragLeave中命中目标同样通过targetElement?.closest([data-block-id])与relatedElement?.closest([data-block-id])计算用于判断拖拽是否离开编辑器或离开当前块见 DndPlugin.tsx。这正是父级委托 data-*命中路由的典型样板。预算指标五个必须跟踪的计数器规则为每一次评审/实现定义了五个量化指标预算指标含义与监控点handler count per repeated unit每个重复单元实际挂载的处理器数量pointer/hover/keyboard/drag/selection 各算一个total active handlers at target cohort目标单元队列如所有块、所有行上当前活跃的处理器总数即单元数 × 每单元处理器数hot handler allocations per render每次渲染过程中热路径上的处理器分配次数闭包是否被重复创建drag/resize event frequency拖拽、缩放resize等连续事件流的触发频率评估其是否按需订阅pointer capture or passive listener policy是否使用setPointerCapture/releasePointerCapture以及addEventListener的passive选项策略touchmove/wheel等滚动相关事件应声明 passive 以避免强制同步布局判定阈值每重复单元 20 处理器即视为 fire规则给出了唯一的硬性判定Treat 20 handlers per repeated unit as a fire.即单个重复单元上挂载超过 20 个处理器直接判定为违规fire必须在评审中打回。这个数字的意义在于即便单个单元 20 个处理器看起来不多一旦乘以 1000 个单元就是 2 万个活跃监听器足以拖垮事件分发与 INPInteraction to Next Paint。结合 interaction-inp-matrix.md 中 p50/p75/p95/p99 的分位监控要求handler 总量超标会最先反映在 type、select、drag selection 等交互的分位延迟上。源码级实践plate 如何落地委托预算plate 仓库中已有多个符合该预算模型的实现可作为评审时的正面样例1. 文档级委托拖拽事件只挂一次DndPlugin.tsx 将dragleave与drop监听在useEffect中挂到document上且只在插件挂载时注册一次useEffect(() { document.addEventListener(dragleave, handleDragLeave, true); document.addEventListener(drop, handleDrop, true); return () { document.removeEventListener(dragleave, handleDragLeave, true); document.removeEventListener(drop, handleDrop, true); }; }, [handleDragLeave, handleDrop]);注意两点一是监听目标选择document而非每个块属于稳定父级二是使用了捕获阶段true确保编辑器外的拖拽离开也能被感知从而正确隐藏 drop line拖放指示线。此外代码注释明确说明drop 要监听在 document 上而不是编辑器内部因为拖拽可能从文件系统发起属于编辑器外的命中场景——委托到更高层父级天然覆盖了这类边界。2. 条件式订阅resize 监听只在拖拽期间存在ResizeHandle.tsx 展示了对drag/resize event frequency预算的经典处理mousemove/mouseup/touchmove/touchend这 4 个高频监听不会常驻而是仅在isResizing为 true 时挂到window松开鼠标后立即通过 cleanup 移除React.useEffect(() { if (!isResizing) return; window.addEventListener(mousemove, handleMouseMove); window.addEventListener(mouseup, handleMouseUp); window.addEventListener(touchmove, handleMouseMove); window.addEventListener(touchend, handleMouseUp); return () { window.removeEventListener(mousemove, handleMouseMove); window.removeEventListener(mouseup, handleMouseUp); window.removeEventListener(touchmove, handleMouseMove); window.removeEventListener(touchend, handleMouseUp); }; }, [isResizing, initialPosition, initialSize, onResize, isHorizontal, onHoverEnd, direction]);这是一个关键设计高频连续事件move绝不常驻用状态门控把每单元常驻监听降为全局、按需、生命周期内临时监听。无论编辑器里有多少个ResizeHandle组件实例window上同一时刻最多只有这 4 个监听器与表格/媒体单元数量完全解耦。这与total active handlers at target cohort预算直接对应——活跃处理器数不随单元数增长。3. 自定义事件总线聚焦/失焦委托useFocusEditorEvents.ts 在document上监听自定义事件focus-editor-event与blur-editor-event再按event.detail.id与目标编辑器 id 匹配后分发const onFocusEditor (event: Event) { const id (event as any).detail.id; if (!!onEditorFocus editorRef editorRef.id id) { onEditorFocus(); } }; document.addEventListener(FOCUS_EDITOR_EVENT, onFocusEditor); document.addEventListener(BLUR_EDITOR_EVENT, onBlurEditor);当一个页面存在多个编辑器实例时每个实例只需在document上注册一次监听用 id 路由到自身而不是让每个编辑器在内部所有节点上挂载聚焦/失焦处理器。这是委托 数据路由在跨实例场景的又一体现。4. 相关委托实现速查仓库内还有多处同类实现可作为对照样例继续阅读useOnClickOutside.ts将点击外部检测委托到 document 级 mousedown/touchstartEmojiObserver.ts用单个 IntersectionObserver 实例观察多个 emoji 单元避免每单元一个观察器SelectionArea.ts选区拖拽的高频指针事件采用按需订阅模式isHotkeyPressed.ts全局键盘状态跟踪避免每单元注册 keydown。与相邻规则的协同本规则不是孤立存在的它隶属于 .agents/rules/performance/rules/ 性能规则族评审时通常与以下规则配合使用repeated-unit-budget.md先为重复单元建立 DOM 节点数、React 组件数、effects、subscriptions、selectors 等整体预算表事件处理器预算只是其中一行其中effects per unit: Are effects banned from wrappers and repeated rows?与本文的每单元处理器直接对应——重复行内既不应有 effect也不应有每单元监听器。interaction-inp-matrix.md从交互结果侧验证预算是否有效——当真实 INP 数据不可用时用event-to-update和event-to-paint计时替代且拒绝只看平均值、只看启动时间、只看虚拟化收益而缺少复制/粘贴/选区跟进行的结论。effect-subscription-budget.md从订阅侧把关确保每个单元按运行时 id/范围窄订阅避免订阅数随文档规模膨胀。评审 Checklist 与常见违规将以上原则整理为可执行的评审清单数一数在目标重复单元组件中统计挂载的 props 级事件处理器onPointerDown、onMouseEnter、onDragStart、onKeyDown、onSelect等超过 20 个即 fire。算总量每单元处理器数 × 单元数确认 target cohort 的活跃处理器总数是否与文档规模线性相关若是应改为父级委托。查闭包在每次 render 中是否有为每个单元新建的箭头函数并直接传入 DOM 事件属性如果是检查能否改用data-*路由到稳定回调。查连续事件mousemove/touchmove/resize/drag是否按需订阅如isResizing门控是否常驻在window/document上且数量为 O(1)。查 passive滚动相关监听touchmove、wheel是否声明{ passive: true }避免滚动路径上的强制同步布局确认 pointer capture 的获取与释放成对出现。验证收益改完委托后用 interaction-inp-matrix.md 的分位指标type、select、drag selection、paste对比前后差异而不是只看单元数下降。总结事件委托预算的核心不是禁止使用事件处理器而是让处理器数量与内容规模解耦重复单元的处理器收敛到稳定父级单元身份交给data-*属性在运行时路由连续事件流按需订阅并声明 passive最终用每单元 ≤20 个处理器和活跃处理器总数不随单元数线性增长两条硬标准守住编辑器在万级单元规模下的交互响应性。plate 仓库中 DndPlugin.tsx、ResizeHandle.tsx、useFocusEditorEvents.ts 分别演示了文档级委托、条件式高频订阅、自定义事件路由三种达标形态可直接作为团队评审与实现时的参照模板。【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考