ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Slate v2 Editable 事件运行时硬切割:把事件组装从 React 组件闭包迁移到 `useEditableEventRuntime` 的完整实战计划

Slate v2 Editable 事件运行时硬切割:把事件组装从 React 组件闭包迁移到 `useEditableEventRuntime` 的完整实战计划 Slate v2 Editable 事件运行时硬切割把事件组装从 React 组件闭包迁移到useEditableEventRuntime的完整实战计划【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate导读本文以仓库中的 docs/plans/2026-04-27-slate-v2-editable-event-runtime-hard-cut-plan.md 为主体完整解析 Slate v2 一次关键的架构硬切割将EditableDOMRoot组件内部以 React 闭包形式组装的全部编辑器事件运行时beforeinput/input、剪贴板、拖拽、合成事件、键盘、焦点鼠标等 20 个处理器迁出到独立的事件运行时门面useEditableEventRuntime(...)与一组runtime-*.ts事件族模块。文章将带你理解React 只负责挂载、事件运行时驱动编辑器的职责边界、四道硬性切割约束、从 Phase 0 到 Phase 8 的分阶段执行与验收门禁以及执行账本中20 个闭包 → 0 个闭包的真实收敛轨迹。读完你可以直接复用这套静态清单锁架构 浏览器行证回归的重构方法论。一、问题背景EditableDOMRoot仍是事件运行时的交通管制中心本次切割发生在一次既有重构之后。此前Editable runtime/root selector lane已经将直接热策略体hot policy bodies从EditableDOMRoot中移出——那是正确的切割但它并不是最终架构。计划文档问题章节明确指出EditableDOMRoot仍然在 React 组件闭包中组装整个编辑器事件运行时具体包括以下 12 类职责beforeinput与 Reactbeforeinputfallbackinput与 input capturepaste、copy、cut、drag、dropcomposition start/update/endfocus、blur、click、mouse down、mouse upkeydownselection import controller 接线repair request 接线kernel frame 与 trace 接线Android manager 接线shell-backed selection 状态迁移browser handle 挂载尽管策略体大多已经落在runtime/strategy模块中组件本身仍然是交通管制中心。对于 React 19.2-perfect runtime 的目标而言这意味着 React 对热路径的所有权依然过重——每一次按键、每一次合成输入都要经过 React 组件闭包这个枢纽这在性能姿态上不可接受。二、北极星React 挂载编辑器事件运行时驱动编辑器计划的 North Star原文只有一句话的职责宣言React attaches the editor. The event runtime drives the editor.EditableDOMRoot应该只做解析 props持有 React refs 与 context providers实例化一个事件运行时 hook挂载返回的稳定 handlers渲染 editable root 与 childrenEditableDOMRoot不应该再做调用prepareEditable*Kernel(...)调用applyEditable*Strategy(...)记录 kernel trace payloads按浏览器编辑策略分支决定 selection import/export 时机在单个事件处理器里直接请求 repair在事件族代码里穿梭forceRender因为 app callback props 变化而重建热处理器这套应做/不应做清单本身就是可复用的架构审查工具任何让 React 组件重新承担上述任一职责的改动都应当被视为架构回退。三、目标形态运行时门面 事件族模块 稳定处理器契约3.1 Runtime FacadeuseEditableEventRuntime计划在slate-react/src/editable/runtime-event-engine.ts新增一个内部事件运行时门面facade首选 hook 形状如下const eventRuntime useEditableEventRuntime({ androidInputManagerRef, attributes, browserHandleNextId, browserHandleRangeRefs, deferredOperations, editor, inputController, inputRules, isShellBackedSelection, largeDocument, onKeyCommand, onUserInput, readOnly, rootRef, scrollSelectionIntoView, setExplicitShellBackedSelection, setIsComposing, shellBackedSelection, });返回形状{ attachBrowserHandle(): void handlers: EditableRootEventHandlers repair: EditableRepairRuntime selection: EditableSelectionRuntime }计划特别强调Exact names can change. Ownership cannot.——接口名可以改但所有权边界不可妥协。EditableDOMRoot最终只消费eventRuntime.handlersspread 或直接赋值到根元素自身不再逐一定义任何handle*闭包。3.2 事件族模块拒绝巨型文件计划明确反对把门面做成新的 god module要求 facade 组合更小粒度的事件族所有者目标模块划分如下模块职责runtime-before-input-events.ts原生 beforeinput 与 React fallback 组装runtime-input-events.tsDOM input / React input / input-capture 组装runtime-clipboard-events.tscopy / cut / pasteruntime-composition-events.tscomposition start / update / endruntime-focus-mouse-events.tsfocus / blur / click / mousedown / mouseupruntime-keyboard-events.tskeydown 组装runtime-drag-events.tsdragstart / dragover / dragend / dropruntime-browser-handle-events.tsbrowser proof handle 挂载原则是既有的 strategy 模块继续作为 worker 存在事件运行时只负责编排事件族不吞并每一个变更算法。这也正是执行账本里反复出现的拒绝策略——不要把 composition 状态全部折叠进 facade那会让runtime-event-engine.ts变成计划明确拒绝的 god module。3.3 稳定 Handler 契约React 19.2 性能姿态计划要求 app callbacks 不得搅动热处理器身份。具体手段是使用 callback refs 或一个内部useLatestEditableProps(...)辅助函数覆盖attributes.onBeforeInput/attributes.onInput/attributes.onKeyDownclipboard callbackscomposition callbacksfocus/mouse callbacksonKeyCommand、inputRules目标契约当 editor/runtime 身份不变时热根处理器应跨普通 app prop callback 变化保持稳定。这对应 React 19.2 性能姿态的四条纪律瞬态热编辑状态放在 refs / runtime 对象中React state 只用于可见渲染事实事件处理器不重新订阅宽泛的编辑器状态非紧急的 proof 或 UI 更新不进入打字路径四、四道硬性切割Hard CutsCut 1EditableDOMRoot停止导入事件 worker切割完成后components/editable.tsx不得再直接导入以下事件 worker 族clipboard-input-strategycomposition-state事件应用器editing-kernelprepare 函数keyboard-input-strategymodel-input-strategynative-input-strategy事件向的selection-reconcilerworkers允许保留的导入仅限于render-only 辅助、contexts、类型以及事件运行时 hook。Cut 2事件运行时拥有处理器组装权全部 21 个根处理器在 runtime 模块中组装onDOMBeforeInput、onReactBeforeInput、onDOMInput、onInputCapture、onPaste、onCopy、onCut、onDragStart、onDragOver、onDragEnd、onDrop、onCompositionStart、onCompositionUpdate、onCompositionEnd、onFocus、onBlur、onClick、onMouseDown、onMouseUp、onKeyDown。EditableDOMRoot只做eventRuntime.handlers的 spread 或赋值。Cut 3Selection 与 Repair 成为运行时输入而非处理器局部事件处理器只能调用命名的运行时能力eventRuntime.selection.flushSelectionChange() eventRuntime.selection.applyKeyDownSelectionPolicy(...) eventRuntime.selection.syncDOMSelectionFromRuntime() eventRuntime.repair.request(...) eventRuntime.trace.record(...)因为处理器本身已经不在EditableDOMRoot中组件内自然不存在直接调用这些能力的机会。Cut 4静态守卫防止回退计划明确写道Do not rely on code review memory. The next rushed patch must fail locally.——不依赖代码评审的记忆下一个赶工补丁必须在本地直接失败。为此要新增守卫当EditableDOMRoot导入或调用被禁事件 worker 时直接报错。五、分阶段执行计划Phase 0–8Phase 0冻结当前事件表面目的证明计划针对的是真实当前表面而不是陈旧债务。行动包括清点EditableDOMRoot中每一个 handler 闭包、清点components/editable.tsx的每一个事件 worker 导入、新增记录当前 forbidden/import 所有者清单的包级契约并把任何允许保留的导入分类为render-only / root ref-context wiring / event runtime facade / 带 burn-down 所有者的临时桥。验收事件导入全部登记在一个守卫中守卫为每个导入族指明最终所有者清点变绿前不动任何行为。涉及文件为slate-react/src/components/editable.tsx、test/kernel-authority-audit-contract.ts、test/surface-contract.tsx。驱动门禁bun --filter slate-react test:vitest test/kernel-authority-audit-contract.test.ts test/surface-contract.test.tsxPhase 1创建事件运行时门面零行为变更新增runtime-event-engine.ts暂不移动任何事件行为定义EditableRootEventHandlers与运行时输入/输出类型把既有运行时引擎selection change runtime、selection import controller、repair runtime、kernel trace runtime、composition runtime、Android runtime穿线进 facade需要处原样返回既有 handler 值。验收EditableDOMRoot可以实例化useEditableEventRuntime(...)既有测试在任何事件族抽取之前全绿无公开 API 变更。驱动门禁bun --filter slate-react typecheck。Phase 2先迁移低风险事件族在触碰最敏感输入路径之前先瘦身依次迁移 (1) copy/cut/paste(2) drag/drop(3) focus/blur/click/mousedown/mouseup。验收要求EditableDOMRoot不再定义这些闭包剪贴板与鼠标选择的 kernel trace 保持完全一致hovering toolbar 在鼠标选择后仍显示工具栏paste/normalize/undo 压力族仍可回放。驱动门禁bun --filter slate-react test:vitest test/editing-kernel-contract.test.ts test/editing-epoch-kernel-contract.test.ts PLAYWRIGHT_RETRIES0 PLAYWRIGHT_WORKERS1 bun playwright playwright/integration/examples/hovering-toolbar.test.ts playwright/integration/examples/richtext.test.ts --projectchromium --grep hovering toolbar|paste|undoPhase 3迁移 Composition 与 Android 事件组装将 composition start/update/end 处理器移入runtime-composition-events.ts状态迁移保留在runtime-composition-engine.tsAndroid 生命周期保留在runtime-android-engine.ts事件运行时负责把 Android ref 接给 composition/input workers。验收composition 处理器不随 app composition callbacks 变化而重建IME 压力行保持 model text、focus owner 与 trace 断言全绿。驱动门禁bun --filter slate-react test:vitest test/editing-epoch-kernel-contract.test.ts STRESS_FAMILIESselection-repair-ime PLAYWRIGHT_RETRIES0 bun test:stressPhase 4迁移 Beforeinput 与 Input 组装最难的 React 拥有路径原生beforeinput组装移入runtime-before-input-events.tsReact fallback 归同一所有者input / input-capture 组装移入runtime-input-events.ts实际变更决策仍留在既有 worker 模块。必须逐条保持 selection import 时机不变在 model-owned beforeinput 之前 flush selectionchange尊重 internal targets保留 Android beforeinput 分支保留 WebKit shadow DOM 分支保留重复 epoch command 守卫保留 model-owned native history repair验收EditableDOMRoot不再导入/调用 beforeinput/input strategy workersnative word-delete 行在关闭重试下保持绿色search highlighting 输入保持焦点placeholder 输入/删除/撤销不回退直接 DOM 文本同步不引入公开 stale selector 策略。驱动门禁bun --filter slate-react test:vitest test/selection-controller-contract.test.ts test/editing-kernel-contract.test.ts test/surface-contract.test.tsx PLAYWRIGHT_RETRIES0 PLAYWRIGHT_WORKERS1 bun playwright playwright/integration/examples/richtext.test.ts playwright/integration/examples/search-highlighting.test.ts playwright/integration/examples/placeholder.test.ts --projectchromium --grep native word-delete|search|placeholderPhase 5迁移 Keydown 组装事件运行时拥有prepareEditableKeyDownKernel(...)、selection policy 应用、keydown event frame 创建、keyboard worker 调用、arrow-up/down 延迟 DOM selection 同步、keydown trace 记录同时保留onKeyCommand、read-only 行为、shell-backed selection 更新与 large-document 策略。验收mentions inline void 双侧导航、表格右箭头单元格边界偏移0、图片/块 void 键盘导航、large-document shell 激活全部保持绿色。驱动门禁bun --filter slate-react test:vitest test/selection-runtime-contract.test.ts test/selection-controller-contract.test.ts PLAYWRIGHT_RETRIES0 PLAYWRIGHT_WORKERS1 bun playwright playwright/integration/examples/mentions.test.ts playwright/integration/examples/tables.test.ts playwright/integration/examples/images.test.ts playwright/integration/examples/large-document-runtime.test.ts --projectchromiumPhase 6迁移 Browser Handle 与 Target Runtime 接线attachSlateBrowserHandle(...)移入runtime-browser-handle-events.tswriteTargetRuntime(...)移入事件运行时门面或小型 target-runtime 桥所有者browser handle force-render 调用在独立 proof-transport 清理落地前继续归类为 proof bridge 调用。验收EditableDOMRoot不再直接挂载 browser handletarget runtime 只有一个所有者browser proof handle 保持 test/proof-only 并被审计。Phase 7收缩EditableDOMRoot并锁定边界移除components/editable.tsx的直接事件 worker 导入EditableDOMRoot降级为纯 wiring/render 组件新增静态契约forbidden imports、forbidden calls、最大容忍的 handler 闭包清单、允许的 runtime facade 导入如可行再补 handler 身份契约app callback prop 变化不应重建热处理器editor/runtime 身份变化可以。验收静态守卫在事件 worker 回归导入或调用prepareEditable*Kernel/applyEditable*/recordKernelEventTrace时直接失败。Phase 8浏览器证明与压力收口证明这不是单纯的文件搬移。必测的聚焦浏览器行包括hovering toolbar 鼠标选择、mentions inline void 双侧导航、表格右箭头单元格边界0、图片/块 void 键盘导航、embeds/块 void 布局与导航、search highlighting 输入焦点保持、placeholder 输入/删除/撤销、richtext 持久 native word-delete、large-document shell 激活与 composition 行。必测压力族包括 7 个inline-void-boundary-navigation、block-void-navigation、table-cell-boundary-navigation、external-decoration-refresh、mouse-selection-toolbar、paste-normalize-undo、selection-repair-ime。最终收口门禁bun --filter slate-react test:vitest bun --filter slate-react typecheck bun --filter slate-react build bun lint:fix # 关闭重试的定向 Chromium 回归包 bun test:stress bun check:full纪律如果bun check:full对某行重试收口前必须单独、关闭重试地重跑该行。六、实现顺序与理由计划给出的顺序原文是Phase 0 inventory guardPhase 1 facade零行为迁移Phase 2 低风险 clipboard/drag/focus/mouse 族Phase 3 composition/Android 组装Phase 4 beforeinput/input 组装Phase 5 keydown 组装Phase 6 browser handle 与 target runtime 桥Phase 7 收缩与静态锁Phase 8 proof 收口理由非常明确beforeinput/input 与 keydown 是最高风险的时序路径所以在触碰它们之前事件运行时必须在结构上已经真实存在。低风险族先行既能在早期拿到确定性收益又能在最敏感路径动刀前积累足够的门禁与证明经验。七、非目标这条 lane 明确不做的事不改动公开 app renderer DX不重写 selection / repair / composition / Android 算法除非失败契约证明抽取暴露了真实 bug不把runtime-event-engine.ts做成塞满所有事件体的巨型文件不通过放宽 React 重渲染来换取浏览器行变绿不给默认bun check增加慢速压力测试不声称 legacy 浏览器对等性那是另一条独立的 current-vs-legacy 生成 harness lane八、停止与重新规划条件计划预定义了六种必须停下重新规划的情形handler 身份稳定要求使用 stale app callbacks事件运行时变成比EditableDOMRoot更糟的 god modulebeforeinput/input 行只能靠放宽forceRender()通过keydown 行只能靠把 DOM selection 直接导回EditableDOMRoot通过静态守卫需要大而模糊的 allowlist浏览器测试在不断言 model selection、DOM selection、focus owner 与 render budget 的情况下通过而这些事实恰恰是关键点九、完成定义这条 lane 只有在以下条件全部满足时才算完成EditableDOMRoot不再组装根事件处理器EditableDOMRoot不再直接导入事件 worker strategy 模块事件族组装位于useEditableEventRuntime(...)之后热根处理器跨普通 app callback prop 变化保持稳定或剩余扰动被显式测量并接受selection、repair、kernel trace、composition、Android、browser handle、target runtime 接线全部成为事件运行时能力静态守卫阻止事件 worker 导入/调用回归EditableDOMRoot聚焦浏览器行与生成压力族通过bun check:full通过后才标记完成十、执行账本复盘从 2021 到 00 的真实收敛计划的执行账本Execution Ledger完整记录了 2026-04-27 当天的推进轨迹是理解这套方法如何落地的最佳教材阶段handler 闭包 / wrapper 常量关键证据激活Phase 0 起点20 / 21EditableDOMRoot直接导入 9 个事件 worker 族Phase 2 检查点8 / 9剪贴板/拖拽/焦点鼠标迁移完成Phase 3 检查点5 / 6composition 组装迁出Phase 4 检查点1 / 1beforeinput/input 组装迁出9 行 chromium 证明Phase 5 检查点0 / 0keydown 迁出33 行 chromium 证明Phase 6 检查点0 / 0browser handle 与 target-runtime 桥迁出Phase 7 最终检查点0 / 0facade 组合完成bun check:full通过最终收口数据来自 Phase 7 检查点authority/surface guard 通过slate-reacttypecheck 通过修掉了 runtime facade 状态类型到EditableInputControllerState的一处问题合并单元门禁 6 文件 50 测试全绿定向包构建通过仅保留既有is-hotkeyexternal 警告聚焦浏览器证明hovering toolbar/richtext/search-highlighting 15 行 mentions/tables/images/large-document 33 行bun lint:fix格式化 10 个文件后各门禁复跑全绿bun check:full通过完整集成扫描 628 passed / 4 skipped。每个检查点都记录了决策 拒绝策略 下一步例如 Phase 4 后明确不要借机扩大为键盘策略Phase 5 后明确不能因为闭包消失就跳过 browser handle/target runtime。这种每步一个可验证证据 明确排除项的账本写法本身也是大型架构迁移值得借鉴的工程纪律。十一、仓库佐证静态清单锁 浏览器行证模式本次计划沉淀的静态清单 浏览器证明双锁模式在仓库的解决方案文档 docs/solutions/developer-experience/2026-04-27-slate-react-runtime-owner-cuts-need-static-inventories-and-browser-proof.md 中有完整记录。它给出的核心洞察是把代码移进辅助函数只能让文件变小无法证明所有权真的变了必须用静态清单作为架构锁再用浏览器证明作为回归锁。典型守卫形态// test/kernel-authority-audit-contract.ts expectAuthorityInventory(/\bbeginEditableEventFrame\(/g, { packages/slate-react/src/editable/runtime-kernel-trace.ts: { count: 3, next: central-owner, owner: Runtime kernel trace engine, rationale: Non-selectionchange event frames are owned by the runtime kernel trace engine., }, })该文档还记录了三条行不通的路径与主计划的决策相互印证把 examples 当安全网行不通示例行无法阻止宽泛 selector/桥接策略潜回EditableDOMRoot无清单的 helper 抽取行不通只跑定向 Chromium 证明不足以收口压力与bun check:full也必须通过。研究决策文档 docs/research/decisions/slate-v2-architecture-verdict-after-human-stress-sweep.md 在 2026-04-28 的状态章节确认了这条 lane 的最终成果EditableDOMRoot将根策略编排委托给useEditableRootRuntime(...)事件处理器组装位于useEditableEventRuntime(...)之后通用根选择器被围栏到root-selector-sources.tsrelease escape-hatch inventory 反映降低后的react-runtime:stale计数bun check:full以 628 通过 / 4 跳过收口——与主计划账本完全一致。十二、可复用的方法论总结所有权先于代码先写清楚组件应做什么、不应做什么的清单再动代码名字可以改所有权不能改。门面必须组合而非吞并facade 只编排事件族策略 worker 继续承担算法细节防止制造新的 god module。先冻结、再迁移、后证明Phase 0 的 inventory guard 先冻结现状任何行为迁移都必须在清点变绿后进行。最危险的路径最后动beforeinput/input 与 keydown 在运行时结构真实存在后才触碰且每步都有针对性的浏览器行与压力族证明。静态守卫替代评审记忆赶工补丁必须在本地直接失败而不是靠 review 拦截。完成定义可量化0 闭包、0 直接导入、facade 唯一入口、bun check:full全绿缺一不可。对任何想重构React 组件闭包承担过多运行时职责这一类问题的团队这份计划从职责宣言、边界切割、分阶段执行到账本复盘都是一份可以直接照搬的完整作战手册。【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表