ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Slate v2 Node Query API 重构指南:从 `match` 到懒遍历 `entries` / `find` / `some`

Slate v2 Node Query API 重构指南:从 `match` 到懒遍历 `entries` / `find` / `some` Slate v2 Node Query API 重构指南从match到懒遍历entries/find/some【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate本文聚焦 plate 仓库中 Slate v2 节点查询 API 的一次核心演进以 2026-05-14-slate-v2-node-query-api-ralplan.md 为骨架讲解如何用懒遍历的state.nodes.entries取代会产生数组物化的state.nodes.match并新增find、some两个早期退出查询助手用于工具栏激活态判断、首个匹配节点获取等高频场景。读完本文你将掌握 Slate v2 节点查询 API 的目标形状、选项语义、内部运行时设计、测试与基准门槛以及该决策在 Legacy Slate / ProseMirror / Lexical / Tiptap 生态坐标系中的位置。1. 背景当前示例形态并非绝对最优Slate v2 的节点查询 API 在演进过程中曾以如下形态出现在示例中const [link] editor.read((state) Array.from( state.nodes.match({ match: (n) NodeApi.isElement(n) n.type link, }), ), );这条代码暴露了两个问题物化浪费底层 v2 遍历虽然是惰性lazy的、基于生成器generator的实现但这个调用点为了读取一个条目却用Array.from把全部匹配结果物化成数组将惰性语义全部丢弃API 口吃stutterstate.nodes.match({ match: ... })中方法名与选项名重复出现同一个词可读性和打字体验都很差。在 plate 仓库当前源码中可以印证这一遍历层的生成器本质查询生成器实现位于 packages/slate/src/internal/editor/nodes.ts签名是export function* nodesN, E(editor, options {})通过yield逐个产出NodeEntry裸树遍历同样由生成器承担packages/slate/src/interfaces/node.ts 中的NodeApi.nodes返回GeneratorNodeEntryN, void, undefined。换句话说遍历层本身是惰性的问题只出在调用点的物化习惯上。2. 目标 APIentriesfindsome规划文档给出的目标形态draft target如下const link editor.read((state) state.nodes.find({ match: (n) NodeApi.isElement(n) n.type link, }), ); const isActive editor.read((state) state.nodes.some({ match: (n) NodeApi.isElement(n) n.type link, }), ); for (const [node, path] of state.nodes.entries({ at, match })) { // lazy all-match traversal }三者分工明确方法语义消费方式entries(options)懒遍历全部匹配节点产出NodeEntry序列for...of逐条消费find(options)返回首个匹配的NodeEntry命中即停直接取值some(options)返回boolean表示是否存在匹配节点命中即停布尔判断2.1 目标类型签名规划文档给出的公开 API 目标形状为type EditorStateNodesApi { entries: T extends Node( options?: EditorNodesOptionsT, ) GeneratorNodeEntryT, void, undefined; find: T extends Node( options?: EditorNodesOptionsT, ) NodeEntryT | undefined; some: T extends Node(options?: EditorNodesOptionsT) boolean; };同时已有的直接访问器保持不变例如above、children、first(at)、get、levels、next、previous、void。这里有一个明确的边界不要对first做查询匹配重载因为first的既有语义是某个位置上的第一个节点重载会造成歧义。在 plate 仓库的公开编辑器 API 面中可以找到与这些语义对应的实现落点editor.api.nodes(options)返回生成器见 packages/slate/src/interfaces/editor/editor-api.tseditor.api.node(...)用于按路径取节点或按选项找首个匹配节点见 packages/slate/src/interfaces/editor/editor-api.ts其 first-match 语义是通过消费生成器的第一个 yield 实现的packages/slate/src/internal/editor/editor-node.ts 中nodeEntries.next().valueeditor.api.some(options)的语义是位置默认为选区上的任意节点满足条件即返回 true实现为!!editor.api.node(options)见 packages/slate/src/internal/editor-extension/some.ts。3. 决策过程五个候选方案与最终选择规划文档将可选方案逐一摆出并给出裁决选项优点缺点裁决仅保留state.nodes.match改动最小现有测试已在使用保留match({ match })口吃且诱导Array.from(...)[0]写法拒绝不是绝对最优新增find/some保留match作为全匹配名称改动小修复常见检查的性能陷阱仍会公开一个别扭的全匹配方法名可行后备全匹配改名entries新增find/someDX 最佳保留惰性语义且结果命名清晰相对当前 v2 草案 API 是破坏性改名当选前提是 pre-release恢复公开静态Editor.nodes(editor, ...)最接近 Legacy Slate 片段对抗已接受的 state/tx 读取生命周期产生两条公开读取路由拒绝照搬 ProseMirror 回调遍历零分配Slate DX 变差无法自然返回首个条目拒绝照搬 Lexical 数组/类型映射查询面对类型索引读取可能很快过度适配 Lexical 的 key/node-map 运行时使 Slate 偏离路径/树原生本切片拒绝照搬 Tiptap 产品级查询助手应用 DX 宽泛querySelector、findChildren、isNodeActive把插件/产品策略塞进裸 Slate诱导 selector/字符串 API核心层拒绝最终选择将惰性全匹配读取 API 改名为state.nodes.entries新增state.nodes.find与state.nodes.some并保留match作为谓词选项名。3.1 为什么拒绝every规划文档专门审计了state.nodes.every(options)候选JavaScript 集合对称性、工具栏全选检查、ProseMirror 回调遍历可以在首个 false 处停止都是它的吸引力所在。但 Slate 当前的EditorNodesOptions中match已经是产出过滤谓词——every({ match })要么对已经过滤过的条目恒真空洞要么需要第二个候选谓词。因此本计划明确拒绝every改用some 否定谓词或产品层助手直到出现干净的 candidate/assertion API 拆分。3.2 其他被拒/延期的候选候选参考压力裁决原因state.nodes.closest/findParentLexical$findMatchingParent、Tiptap 父级助手拒绝Slate 已有above是既定的路径/位置感知祖先查询querySelector/querySelectorAllTiptapNodePosselector DX核心层拒绝selector 字符串是产品层策略无法干净映射到无意见的节点形状、自定义元素类型与路径选项findChildren/findChildrenInRangeTiptap 助手面核心层拒绝entries({ at, match })已是原始原语产品助手应放 Plate 或示例层state.nodes.count常见应用便利延期可避免数组分配但仍需全遍历除非引入 limit等真实调用点证明热点toArray/filter/mapLexical/Tiptap 数组重助手由后续计划拆分裸核心拒绝filter/map窄分配显式toArray(options, map?)由 generator-materialization 计划重新开启按元素类型的类型索引查找Lexical 只读 type-to-node map仅基准验证的未来车道可加速重复全局类型查询但改变内存/更新复杂度需基准证明 DFS 是瓶颈4. 生态坐标系从四个编辑器系统中汲取什么规划文档对比了四个系统的查询机制并给出窃取 / 拒绝结论原文表格整理如下系统来源机制避免借鉴拒绝Slate 目标裁决Legacy Slatepackages/slate/src/editor/nodes.ts生成器式Editor.nodes遍历为取首条目做全量数组工作惰性节点条目迭代语义静态 editor-first 公开路由作为 v2 常态 APIstate.nodes.entries(...)惰性可迭代部分采纳ProseMirrorprosemirror-model的 node/fragment回调遍历 false剪枝核心遍历中的分配零分配遍历与剪枝纪律纯回调公开 DX既有pass 惰性 entries部分采纳LexicalLexicalEditorState/LexicalSelection/LexicalUtils读取生命周期、缓存选区数组、可选的只读类型映射不安全读取与类型组的重复全扫描保留读取生命周期仅在基准证明后考虑索引Slate DFS 查询的数组返回形态editor.read 惰性entries/find/some部分采纳TiptapNodePos/findChildren/isNodeActive产品助手返回数组querySelector有首项逃生常见查询的产品 DX 差首匹配与激活检查的便利性作为裸核心法则的产品层数组助手与 selector 字符串核心层find/some数组仅由调用方展开部分采纳5. 选项参数详解EditorNodesOptions的源码语义find、some、entries三个方法共享同一套查询选项类型EditorNodesOptions其定义位于 packages/slate/src/interfaces/editor/editor-api.tsexport type EditorNodesOptionsV extends Value Value { /** Where to start at. default editor.selection */ at?: At | Span; ignoreNonSelectable?: boolean; reverse?: boolean; universal?: boolean; } OmitQueryOptionsV, at QueryMode QueryVoids;各组成部分在源码中的含义如下at遍历起点默认是editor.selection接受AtPath / Point / Range或Span。在 packages/slate/src/internal/editor/nodes.ts 中at getAt(editor, _options.at) ?? editor.selection若at为空则直接return生成器不产出任何条目。ignoreNonSelectable为 true 时跳过不可选择节点。reverse是否反向遍历。universal要求匹配在每条分支都出现时才产出实现上先收集再统一 yield。QueryOptionseditor-api.ts提供便捷匹配id?: boolean | string—— 按节点 id 匹配true匹配所有带 id 的节点block?: boolean—— 匹配块节点empty?: boolean—— true 只匹配空节点、false 只匹配非空节点match?: PredicateNodeInV—— 核心谓词接受函数或对象utils/match.ts 中对象谓词的语义是每个 key/value 都出现在目标节点上text?: boolean—— true 只匹配文本节点。QueryModeeditor-api.tsall默认返回所有匹配节点highest在层级中只返回最高层匹配节点lowest只返回最低层匹配节点。QueryVoidseditor-api.tsvoids?: boolean为 true 时包含 void 节点。在裸树层NodeApi.nodes使用更精简的NodeNodesOptionsnode.tsfrom/to界定路径区间reverse控制方向pass谓词用于剪枝返回 true 则跳过该节点子树等价于 ProseMirror 回调遍历的 prune-by-false 纪律。5.1 遍历引擎内部做了什么packages/slate/src/internal/editor/nodes.ts 展示了查询层如何委托给裸树遍历它把at换算成from/to路径区间并构造pass回调默认剪掉非 void 模式下isVoid或isElementReadOnly的子树、以及ignoreNonSelectable模式下不可选择的子树。随后在匹配循环里处理mode highest跳过比上次命中更低的节点与mode lowest延迟一拍保证发出的是已确认最低的命中universal则先累积再统一产出nodes.ts。6. 内部运行时目标生成器保留助手早期退出规划文档对运行时实现提出明确纪律entries委托给既有getNodes(editor, options)生成器find用for (const entry of getNodes(...)) return entry实现some用for (const _ of getNodes(...)) return true实现助手内部不允许出现Array.from本切片不引入全局类型索引。这样既保住了默认惰性遍历的架构底线又让最常见的两个消费模式取首条、判存在从物化整个数组退化为遍历到命中即停。7. DX 目标与使用指南规划文档把新 API 落实到四个典型使用场景工具栏激活态检查使用state.nodes.some(...)例如判断当前选区是否包含 link 节点避免物化数组全选uniform selection检查可用some 否定谓词或 Plate 层助手裸 Slate 暂时不应增加语义模糊的every需要拿到实际节点使用state.nodes.find(...)结构变换与 DOM 桥接代码使用for...of state.nodes.entries显式惰性迭代。同时示例代码被要求停止教授Array.from(...)[0]这类首匹配写法因为它既物化全量结果又让调用者误以为遍历成本与数组长度无关。8. Plate 与 Slate-Yjs 迁移骨架Plate可以在find、some、entries之上构建产品助手而不必包装每个核心调用也不必恢复静态Editor.nodes。规划的目标是一个小型底层基质substrate而非直接复刻 Plate 现有公开 API。Slate-Yjs无直接的协同数据模型改动。确定性的惰性查询顺序对插件与归一化决策仍然重要但本计划不声称任何序列化操作或远端应用remote-apply能力。9. 测试与基准门槛规划为这次公开 API 变更设置了可量化的验收门槛早期退出访问计数测试visit-count证明find在首个匹配后停止遍历some命中即返回序列一致性测试entries与旧state.nodes.match产出相同序列覆盖reverse、pass、voids与各mode反向顺序回归#5080 修复的反向迭代顺序不得因改名/别名而回退公共面类型测试entries、find、some的类型签名必须通过 typecheck。基准验收阈值针对 Ralph 的聚焦查询助手基准首匹配位于开头时find与some访问的节点数不得超过匹配前缀 当前遍历所需祖先数首匹配位于末尾 / 无匹配时中位数不得比当前entries遍历差超过 5%五次暖样本首匹配位于开头时在 10k 块文档上find/some至少要比Array.from(entries(...))[0]快 10 倍否则基准必须解释遍历设置为何占主导并仍需证明早期退出访问计数任何助手内部都不得分配全匹配数组。10. 执行结果Ralph 落地验证规划文档记录的执行结果Ralph Execution Result2026-05-14显示该方案已完成落地新增state.nodes.entries(options)作为惰性全匹配查询 API新增state.nodes.find(options)与state.nodes.some(options)早期退出助手从 state/tx 节点面切掉公开草案 APIstate.nodes.match(options)将首方示例与 DOM 内部实现从Array.from(state.nodes.match(...))首匹配模式迁移走扩展query-ref-observation.mjs加入 first-match array /find/some/ last-match / no-match 车道。验证命令与结果以文档记录为准# cwd: .tmp/slate-v2 bun test ./packages/slate/test/query-contract.ts # result: 80 pass, 0 fail bun --filter slate typecheck # pass bun --filter slate-dom typecheck # pass bun typecheck:site # pass bun check # pass含 lint、package/site/root typecheck、Bun tests、slate-react Vitest 套件 bun ./scripts/benchmarks/core/current/query-ref-observation.mjs # 默认 200-block 运行firstMatchArrayMs mean 23.20ms # firstMatchFindMs mean 0.45msfirstMatchSomeMs mean 0.28ms DRIFT_BENCH_BLOCKS10000 DRIFT_BENCH_QUERY_OPS20 \ DRIFT_BENCH_WRITE_OPS5 DRIFT_BENCH_REFS5 DRIFT_BENCH_ITERATIONS3 \ bun ./scripts/benchmarks/core/current/query-ref-observation.mjs # 10k-block 运行firstMatchArrayMs mean 190.70ms # firstMatchFindMs mean 0.23msfirstMatchSomeMs mean 0.11ms # lastMatchFindMs mean 135.88msnoMatchFindMs mean 123.50ms值得注意的两组数字200 块文档上数组物化首匹配耗时约 23.20ms而find/some分别只需 0.45ms 与 0.28ms在 10k 块文档上差距进一步拉大——物化路径达 190.70msfind/some仍保持在亚毫秒级0.23ms / 0.11ms且远超规划设定的10 倍加速门槛。文档还记录了rg清理门禁的通过情况rg -n Array\\.from\\(\\s*state\\.nodes\\.(match|entries)|...|state\\.nodes\\.match\\(|... packages site/examples/ts scripts无匹配说明示例与内部代码中的旧写法已被清除。11. 硬切策略、别名政策与残余风险11.1 硬切与别名政策规划默认硬切hard cut把state.nodes.match切为state.nodes.entries。判断依据是本地构建的dist虽包含草案 API但包 changelog 中没有 v2/state-query 的发布记录因此按 pre-release 本地构建状态处理。若发布负责人证明state.nodes.match已在仓库外发布保留一个仅一个周期one cycle的废弃别名指向entries且示例与文档全部迁移到entries、find、some无论别名是否存在find/some都照常新增不因别名政策而放弃早期退出助手。11.2 高危预检Pre-mortem由于涉及公开 API 变更规划触发了 High-Risk Deliberate Mode预先列出三个失败模式别名政策混乱导致match与entries永远同时出现在示例中find/some内部误用Array.from只改善了 DX 却没有改善性能改名破坏扩展 state 组或 tx 组——两者都通过展开state.nodes进入事务节点。对应的证明计划是公共面类型测试、entries序列一致性测试、find/some早期退出访问计数测试、reverse/pass/voids回归测试以及示例 grep 禁令禁止Array.from(state.nodes.entries(...))[0]风格。11.3 残余风险执行记录披露了一个诚实的前提.tmp/slate-v2在执行前已存在与本切片无关的脏示例/运行时文件如embeds.tsx、images.tsx、paste-html.tsx、rendering-strategy-runtime.tsx及相关示例注册/测试文件它们未被回退也不属于本切片声明范围。12. 总结这套查询 API 的北极星规划文档的 Source-Backed Architecture North Star 总结了 Slate v2 查询层应当坚守的五条原则读/写生命周期优先editor.read/editor.update默认惰性遍历结构代码使用显式全匹配迭代entries常见激活检查使用首匹配助手find/some核心层只提供无意见的原语不塞产品/插件快捷方式。这套原则直接映射到 plate 仓库的源码事实editor.api.nodes返回生成器editor-api.tseditor.api.node通过生成器首个 yield 实现 first-matcheditor-node.tseditor.api.some复用它做布尔判断some.ts——即生成器 早期退出这一设计在当前仓库的公开 API 面上已形成闭环。对于在 plate 之上构建编辑器的团队这套查询面既是性能护栏也是插件友好、可被 Plate / slate-yjs 直接复用的迁移骨架。【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表