ARTICLE DETAIL

资讯详情

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

Slate v2 大文档范围删除性能重构:replace_children 子区间替换操作的规划与落地

Slate v2 大文档范围删除性能重构:replace_children 子区间替换操作的规划与落地 Slate v2 大文档范围删除性能重构replace_children 子区间替换操作的规划与落地【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate本文是 plate 仓库中 Slate v2 开发线的技术规划与实现复盘核心主题是为大文档场景下的剪切/删除Issue #5992引入一种新的核心操作原语replace_children把删除选中的整块顶层子节点从逐个remove_node循环收敛为一次父子数组窗口替换从而把操作成本从与文档规模成正比降为与选中范围成正比。读完本文你将掌握这一操作契约的设计动机、候选方案取舍、内部运行时降级路径、回归证明矩阵、基准阈值以及最终落地后的实测数据与 Issue 状态判定逻辑。1. 问题背景Issue #5992 与大文档剪切成本Slate v2 开发线追踪的 Issue #5992 是一个经典的大文档编辑性能问题在超大文档中使用剪切cut功能耗时严重。gitcrawl 实时台账 中的原始记录为In the case of large documents, using the cut function took a lot of time.大文档下使用剪切功能非常耗时归类为 bug所属集群为 large-document-edit-performance。该计划文档记录的最新技术状态在 50,000 个块的文档中执行选中两个节点后剪切这一最小负载copy-plus-delete复制加删除耗时621.26msedit-only仅编辑删除耗时511.47ms共产生3个操作。当前证明已将其从早期多秒级的旧主人owner改善到毫秒级但距离可接受的交互目标仍有差距因此台账中 #5992 的状态是Improves而非Fixes见 issue-coverage-matrix.md 第 315 行以及 benchmark-candidate-map.md 的 #5992 小节。1.1 为什么逐个remove_node循环不是答案当前删除路径的实现计划文档引用 slate-v2 开发树中的transforms-text/delete-text.ts在检测到精确的整块顶层范围后会从endIndex到startIndex循环对每一个被删除的顶层子节点单独发出一条remove_node操作。每个remove_node都要完整走一遍父节点 children 数组的一次整体替换见 operation.ts 中remove_node的类型定义nodepathselection / path 变换dirty path 分类快照与提交snapshot/commit工作。也就是说删除 N 个块就产生 N 条操作、N 次完整的变换与提交管线。这正是操作成本随文档规模线性放大的根源——而实际上用户只改了 2 个块。计划文档的结论非常直接3个操作比旧行为好但不要用当前remove_node循环来关闭 #5992。1.2 为什么虚拟化不是答案计划文档明确划出了边界不要用虚拟化virtualization作为剪切/删除的修复手段。理由是该成本是模型与操作成本model and operation cost不是DOM/渲染成本DOM/render cost。虚拟化解决的是渲染层的问题而 #5992 的瓶颈在数据模型层的操作放大两者不能互相替代。这一判断也被列入第 17 节的硬性切割Hard Cuts清单。2. 决策论证候选方案与取舍计划文档的决策章节Decision Brief给出五个候选方案并逐一裁决核心决策原则有四条Slate 操作仍然是对外的模型external model一次用户剪切/删除不应变成许多条结构化操作操作负载应与被改变的范围成正比而不是与整个文档成正比除非是有意整篇替换协作collaboration与历史history需要确定性的逆操作inverse与重放replay。候选方案裁决理由保留当前remove_node循环拒绝reject每个被删子节点发一条remove_node每条都经过子数组替换、selection/path 变换、dirty 分类、快照/提交工作用根级replace_fragment做剪切/删除仅过渡transitional only只产生一条操作但在大文档的小范围编辑中存储了完整的旧/新子数组负载形状对 #5992 是错的新增delete_fragment拒绝reject删除本质就是newChildren: []的替换独立的删除操作会重复变换/逆操作逻辑新增/泛化为replace_children硬化后选择choose after hardening符合 Slate 形状、按范围缩放、对逆操作友好、兼容粘贴最接近 ProseMirror 的 replace-step 经验且不照搬整数位置模型在操作系统之外做可变根子数组拼接拒绝reject对 history/collaboration 隐藏真实变更重新引入快照绕过风险2.1replace_fragment的定位正确的证明产物错误的操作名replace_fragment是此前最佳粘贴策略计划留下的证明产物proof artifact它证明了单次操作完成顶层子数组替换是可行的。但计划文档给出了三条否定性判断命名是 paste 形状的fragment语义来自剪贴板/粘贴而引擎真正需要的原语是父节点的子区间拼接a parent child-range splice不应以产品事件paste来命名模型机制负载过宽根级使用时携带完整children与newChildren数组对大文档中的小范围删除来说内存与复制成本都不合适不宜冻结为最终操作实现期间应把当前的语义用途迁移到replace_children仅在需要安全落地时才保留临时内部桥接发布前移除。2.2 命名决策为什么是replace_children而不是splice_children计划文档的维护者反对意见账本记录了一个有代表性的争论为什么不直接叫splice_childrenJS 数组拼接语义结论是 Slate 的操作命名传统是描述模型动作insert_node、remove_node、set_node、split_node、merge_node而不是原始 JS API。replace_children对 undo/collab 负载更清晰也更贴近tx.value.replace的既有命名习惯。同时delete_fragment因过窄被拒绝。3. 目标操作契约ReplaceChildrenOperation 详解计划文档接受的核心实现目标是如下操作类型type ReplaceChildrenOperationV extends Value Value { type: replace_children; path: Path; index: number; children: DescendantInV[]; newChildren: DescendantInV[]; selection: Range | null; newSelection: Range | null; };各字段语义path父节点路径。path: []表示根节点自身此时若范围覆盖全部子节点即为整篇文档替换index被替换子窗口的起始下标children被移除的旧子节点窗口仅限被移除的窗口而不是整个父节点的全部子节点newChildren替换后的新子节点窗口删除场景下为[]selection/newSelection操作前后选择范围用于让选择修复内聚到操作契约中而非事后修补。该操作作为下述场景的统一实现基座删除整块顶层子范围粘贴时替换选中的顶层块当path: []且范围覆盖全部子节点时的整篇文档替换未来的 fragment 拟合fragment fitting替换父节点内部的一个子窗口。对比当前仓库快照中 operation.ts 列出的经典操作族insert_node、remove_node、set_node、split_node、merge_node、move_node、insert_text、remove_text、set_selection可以看到replace_children是首个父子数组窗口级的结构化操作它把过去由多条insert_node/remove_node组合表达的变更压缩为一条自包含的操作且负载与变更窗口成正比。3.1 对应用层透明事务用法保持不变尽管新增了核心操作应用层代码不需要任何改动editor.update((tx) { tx.text.delete({ at: selection }); });计划文档明确要求除非最终操作表面operation surface被刻意设为公开否则公共文档不应教用户手写构造该操作。普通应用代码保持editor.update形态不变——这一点与当前仓库中 editor-transforms.ts 等提供的变换层设计一脉相承。3.2 内部降级Internal Lowering规则精确的子范围删除 → 降级为replace_children其中newChildren: []顶层粘贴替换 → 降级为replace_children携带插入的子节点整篇文档替换 → 使用path: []、index: 0的replace_children。4. 内部运行时流程从删除到单次子区间替换计划文档给出了目标运行时流水线tx.text.delete({ at }) - detect exact replaceable child window - compute parent path index removed children new children - apply replace_children once - map selection to newSelection - classify dirty parent changed child window - history stores one inverse - collab can lower one deterministic child splice该操作被明确要求不得当只改变两个子节点时用完整的根子数组重建操作负载对 #5992 精确整子范围逐子节点发remove_node在操作应用之外直接变异根子数组绕过 selection/path/ref 变换契约。4.1 历史History侧一条操作 一条逆操作一条逻辑操作 一条逆操作的设计与当前仓库的历史机制完全吻合。在 with-history.ts 中undo 的实现是取批次的全部操作、逐个OperationApi.inverse求逆后反转顺序再依次应用见第 69–74 行redo 则原序重放并恢复selectionAfter。因此操作数量越少、每一条操作的逆变换越确定undo/redo 的语义就越稳定。replace_children的逆操作只需交换children与newChildren两个窗口数组天然可逆。4.2 与现有 fragment 提取路径的关系当前仓库中NodeApi.fragment提供取 root 内某 range 对应的切片片段能力见 node.ts 第 113–115 行的接口文档与第 222–229 行的实现getFragment.ts 进一步将其包装为编辑器级 API。计划文档中整块顶层子 fragment 快速路径正是这一能力的演进提取与替换从整文档切片收敛为受控子窗口二者共用同一套路径/range 语义保证复制与粘贴两侧形状一致。5. 生态系统策略从 Lexical / ProseMirror / Tiptap 借鉴计划文档第 6 节给出了跨编辑器生态的策略综合结论是策略性偷师而非照搬系统机制借鉴点拒绝点Slate 目标裁决Lexicaleditor.update dirty leaves/elements 生命周期标签dirty 运行时桶与更新标签供 commit 消费者使用class 节点与$helper API一条子区间操作 dirty parent/range 元数据部分采纳ProseMirror事务累积 steps 并映射 selectionreplace-step 纪律与映射后的 selection整数位置模型与 schema 优先的身份体系replace_children附带 path/index/range 变换语义认同Tiptapcommand/chain 糖包裹单一事务扩展 DX 停留在事务引擎之上把 command chain 当作必需的 Slate API保留editor.update产品糖可降级为单条操作部分采纳Slate v2 现状replace_fragment证明 remove_node删除循环复用证明表面与测试paste 形状的操作名 小范围大文档场景下的全数组负载泛化为replace_children修订综合策略可表达为Lexical-style dirty metadata ProseMirror-style range replacement Slate paths/runtime ids Tiptap-like extension sugar above the engine相关的源研究文档在本仓库中可继续深入ProseMirror 事务/视图/DOM 运行时、Lexical 读/更新扩展运行时、Tiptap 扩展命令与 React DX。6. Plate 与 slate-yjs 的迁移骨架6.1 Plate 侧零产品 API 变化Plate 应当看到的是相同的editor.update写作形态大范围删除/剪切时更少的操作数对 history/collab 桥更干净的操作负载没有新的 Plate 自有的粘贴/删除策略。Plate不需要围绕 #5992 的兼容包装、选择加入快速路径的产品命令、或用来掩盖核心删除成本的虚拟化。这是典型的核心只做模型操作、产品层零感知设计。6.2 slate-yjs 侧直接消费或单事务降级已接受的协作骨架yjs/collab 适配器在能表达父节点子拼接时应直接消费replace_children若传输层无法原子表达则适配器在一个远程事务内将其降级为 remove/insert 操作本地 Slate 历史仍然只看到一条逻辑操作与一条逆操作远程重放必须与本地重放通过相同的 path/index/window 语义收敛。硬性门槛在协作重放/降级有聚焦的契约测试之前或发布说明显式标注首个切片不支持协作降级之前replace_children不算发布就绪。这一门槛同样体现在第 11 节的协作重放/降级契约测试或显式门禁表述中。7. 回归证明矩阵与浏览器压测门槛7.1 遗留回归证明矩阵任何变更都必须覆盖以下行为证明行为必需证明删除一个选中的顶层块一条操作 正确的选择在 50,000 块文档中删除两个选中顶层块达标的延迟目标、一条逻辑替换、子节点正确剪切选中的顶层块复制 fragment 正确、删除后模型正确、一条历史记录文档首/尾删除选择落在合法邻居或编辑器首/尾删除整篇文档保留默认文档策略或显式替换行为删除嵌套列表范围既有列表删除测试保持绿色或显式回退删除 inline/void 范围既有 inline/void 行为保持绿色撤销/重做逆操作恢复被删子节点与选择path/point/range refs被删范围内的 refs 置空范围之后的 refs 按 delta 平移协作重放本地与远程重放收敛dirty paths/runtime ids父节点与受影响的顶层范围失效而非整个文档除非必要7.2 浏览器压测命令与 #5992 阈值在声明任何Fixes #5992之前至少需要以下行在 slate-v2 开发树中执行SLATE_CLIPBOARD_BENCH_HUGE_CUT_BLOCKS50000 SLATE_CLIPBOARD_BENCH_ISSUE_TARGETS1 bun ./scripts/benchmarks/slate/5945-large-plaintext-paste.mjs bun test ./packages/slate/test/delete-contract.ts bun test ./packages/slate/test/clipboard-contract.ts bun test ./packages/slate/test/operations-contract.ts PLAYWRIGHT_RETRIES0 bunx playwright test playwright/stress/generated-editing.test.ts -g paste-normalize-undo --projectchromium若基准推进到Fixes #5992还需要新增浏览器剪切行因为 Issue 描述的是cut function剪切函数不仅仅是纯模型删除。已接受的 #5992 阈值50,000 块双节点 edit-only 剪切本地基准 lane 上 p50150ms50,000 块双节点 copy-plus-delete同 lane 上 p50250ms操作数一条replace_children结构化操作仅当操作契约无法携带newSelection时才允许额外的显式选择操作负载旧/新子数组仅限被移除/插入的窗口而非整个 50,000 子节点的根若以上目标无实质改善则停止归因于操作数下一个归因点是快照/索引分配snapshot/index allocation。8. 高风险预演Pre-Mortem与硬性切割由于本变更涉及操作/数据模型行为计划文档触发了高风险刻意模式预演列出五个失败场景及对应证明路径replace_children之后的 path/point/range 变换错误 → 被删范围之后的选择 refs 漂移历史逆操作恢复了子节点但未恢复选择 → 破坏剪切周边的 undo/redo协作适配器将其解释为全父节点替换 → 丢失意图或产生远程冲突dirty-path 分类过宽 → 只是把 #5992 的成本搬进 React/运行时失效负载存储过多旧/新文档状态 → 大文档上的内存回归。对应证明计划单元级op 校验、inverse、path/point/range 变换、核心级删除契约、粘贴替换契约、历史重放、基准级#5992 issue 规模行 已接受阈值、浏览器级推进到Fixes时的剪切/粘贴/撤销行、协作级重放/降级契约或显式门禁。硬性切割清单明确不做不用虚拟化修复模型删除成本不提供公开的editor.cutFast或应用自选不用全根replace_fragment负载作为小范围子节点删除的最终答案不用多条remove_node循环关闭 #5992不单凭操作数就宣称 #5992 已修复。9. 维护者反对意见账本计划文档第 18 节以账本形式预演了维护者最可能的三个反对意见并给出钢化反驳steelman antithesis与取舍变更可能的反对裁决新增/泛化replace_children操作Slate 操作应当是小而原始的小操作更容易变换与推理保留。问题恰恰在于原始循环在规模化时变得病态ProseMirror 式范围替换才是复合子变更的正确原语替换或降级replace_fragment你刚加了它为什么又要变保留。证明是对的但名称与负载对 delete/cut 过于 paste 化最好在公开冻结前硬切割不叫splice_childrensplice 就是精确的数组原语保留。Slate 操作名描述模型动作而非 JS APIreplace_children对 undo/collab 负载更清晰10. 实施阶段TDD 路线与快驱动门禁计划文档第 22 节给出七个实施阶段明确此 ralplan 到达 done 之前不得执行红色证明Red proof新增replace_children操作契约测试新增 #5992 edit-only 剪切基准目标行断言旧行仍高于目标操作核心Operation coreop 类型、校验、inverse、apply、dirty paths、runtime-index 失效、path/point/range 变换删除降级Delete lowering把精确整子范围删除降级为单条replace_children粘贴迁移Paste migration将当前顶层replace_fragment用法迁移到replace_children历史/协作History/collab证明 inverse、undo/redo、远程重放/降级基准/浏览器Benchmark/browser运行 #5992 issue 规模基准与聚焦的浏览器剪切/粘贴/撤销行文档/台账Docs/ledgers更新 issue 覆盖矩阵、fork 档案、PR 描述与计划状态。执行期间的快驱动门禁fast driver gates在 slate-v2 开发树中运行bun test ./packages/slate/test/delete-contract.ts bun test ./packages/slate/test/clipboard-contract.ts bun test ./packages/slate/test/operations-contract.ts bun --filter slate typecheck SLATE_CLIPBOARD_BENCH_HUGE_CUT_BLOCKS50000 SLATE_CLIPBOARD_BENCH_ISSUE_TARGETS1 bun ./scripts/benchmarks/slate/5945-large-plaintext-paste.mjs浏览器证明Fixes #5992之前必须PLAYWRIGHT_RETRIES0 bunx playwright test playwright/stress/generated-editing.test.ts -g paste-normalize-undo --projectchromium11. Ralph 执行结果与最终基准计划文档第 26 节记录了ralph执行的结果状态complete for the accepted execution lane。已实现的清单replace_children操作类型、校验、inverse、apply、dirty paths、runtime-index 失效、path/point ref 变换精确整块顶层子范围删除降级为单条replace_children粘贴快速路径从replace_fragment迁移到replace_childrenroot/block 子窗口替换DOM 纯文本粘贴回退路径迁移到replace_childrenhistory/collab 重放证明分离冷快照分配与暖编辑器交互延迟的基准目标行大文档剪切的浏览器压力行 既有 paste/normalize/undo 行。验证命令bun test ./packages/slate/test/operations-contract.ts ./packages/slate/test/delete-contract.ts ./packages/slate/test/collab-history-runtime-contract.ts ./packages/slate/test/clipboard-contract.ts bun --filter slate typecheck bun --filter slate-dom typecheck bun lint:fix SLATE_CLIPBOARD_BENCH_HUGE_CUT_BLOCKS50000 SLATE_CLIPBOARD_BENCH_HUGE_CUT_ITERATIONS3 SLATE_CLIPBOARD_BENCH_ISSUE_TARGETS1 SLATE_CLIPBOARD_BENCH_ISSUE_ITERATIONS1 bun ./scripts/benchmarks/slate/5945-large-plaintext-paste.mjs STRESS_FAMILIEShuge-document-cut,paste-normalize-undo PLAYWRIGHT_RETRIES0 bunx playwright test playwright/stress/generated-editing.test.ts -g huge-document-cut|paste-normalize-undo --projectchromium11.1 最新 #5992 基准计划文档记录暖编辑warm editp509.95ms暖复制加删除warm copy-plus-deletep508.62ms操作数1冷编辑cold editp50单独追踪171.91ms。对照规划阶段的621.26mscopy-plus-delete与511.47msedit-only暖路径的实测延迟下降了两个数量级且操作数从 3 收敛到 1——这正是负载与变更窗口成正比的量化验证。注意冷路径cold snapshot allocation仍被单独追踪计划文档明确将快照/索引分配列为下一层可能的成本归属。12. 结论为什么 #5992 仍保持 Improves 而非 Fixes尽管实测数据已大幅改善计划文档与 issue-coverage-matrix.md 台账都坚持 #5992 维持Improves而非Fixes原因有三接受标准是维护者接受度50,000 块基准 5,000 块浏览器压力行需要维护者认可其与原始复现路径cut function等价这属于人为决策不由实现方单方面裁定浏览器层证据要求Issue 描述的是剪切函数而非纯模型删除因此推进到Fixes必须追加浏览器剪切行历史路径证明未完成台账中明确 exact closure remains maintainer acceptance plus historical-path proof精确关闭仍需要维护者接受加历史路径证明。该判定逻辑与 benchmark-candidate-map.md 第 55–71 行的 #5992 条目一致ready-with-minor-setup基准接缝为 huge-document cut benchmark主指标是随文档规模增长的剪切延迟次指标是剪切路径中任何可见的选择或规范化放大。13. 相关 Issue 生态与后续方向计划文档第 12 节的 issue 账本把这次变更放入更广的生态坐标系#2288operation-granularity-and-range-steps被明确升级为范围能力操作的架构支撑——它要求selectAll delete 不应爆炸成大量操作#6038、#5811、#3534、#3551、#4104、#5089、#5630 等分别从批处理引擎、规范化、历史选择状态、inline-void、多块粘贴形状、select-all 粘贴等角度被标记为Related相关但不由本操作单独关闭。后续的 CRDT/Yjs 专项计划将决定远程传输是直接消费replace_children还是在单个远程事务内降级为 remove/insert——这对应计划文档第 2 节中唯一保留的待用户决策点的演进方向。14. 总结replace_children是 Slate v2 开发线对大文档范围编辑性能的一次核心模型级重构它把剪切/删除/粘贴替换统一收敛为一条按变更窗口缩放的父子数组替换操作通过历史逆操作、协作降级、ref 变换与 dirty-path 分类的协同契约把 50,000 块文档中的双节点剪切从数百毫秒压到个位数毫秒暖路径。对于 Plate 与 slate-yjs 的使用者这一变更完全透明——应用代码依旧是editor.update产品粘贴策略依旧归 Plate/应用所有。本文所依据的完整规划与执行记录见 2026-05-06-slate-v2-range-delete-replace-children-ralplan.md当前仓库中可继续对照的源码与台账包括 operation.ts经典操作族、with-history.ts逆操作重放机制、node.tsfragment 提取接口以及 issue-coverage-matrix.md#5992 状态追踪。【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表