Transforms 与 Normalization 机制
1. Transforms 是“语义 API”,不是最终执行层
Transforms.insertNodes / setNodes / moveNodes / select 这些 API 本质是便捷层。
真正落地由 editor.apply(op) 驱动。
官方文档 docs/concepts/04-transforms.md 也明确:复杂 transform 会拆成多个低层 operation。
2. Normalization 的本质:自动修复而非仅校验
Slate 的 normalize 思路是“发现不合法结构 -> 立即修复”,而不是返回 boolean。
默认内建约束(见 docs/concepts/11-normalizing.md)包括:
- Element 必须至少有一个 Text 后代
- 相邻同格式 Text 会合并
- block/inline 子节点结构必须一致
- inline 不能孤立在头尾或紧邻 inline
- editor 顶层只能是 block
3. 关键实现:normalizeNode + 脏路径循环
3.1 默认 normalizer
文件:packages/slate/src/core/normalize-node.ts
核心逻辑:
- 非文本节点缺少
children时补齐 - 空元素插入空文本子节点
- inline 周围补空文本
- 相邻文本可合并时 merge
- 非法 block/inline 混合时 unwrap/remove/wrap
源码注释(节选):
// Ensure that elements have at least one child.
if (element !== editor && element.children.length === 0) {
Transforms.insertNodes(editor, { text: '' }, { at: path.concat(0), voids: true })
}
// Ensure that inline nodes are surrounded by text nodes.3.2 normalize 循环调度
文件:packages/slate/src/editor/normalize.ts
- 维护 dirty path 队列
- 先跑“空 children”早期修复(避免后续规则失效)
- 多轮循环调用
editor.normalizeNode - 每轮可能新增 dirty path(多阶段修复)
4. 为什么会有 withoutNormalizing
批量变换时,中间态常常不合法。
如果每步都 normalize,可能:
- 性能差(重复修复)
- 逻辑错(中间态被提前修复导致后续步骤失效)
所以要用:
Editor.withoutNormalizing(editor, () => {
// 多个 transforms
})文档与 issue 都强调这一点:
- Issue #3467:名字容易误解,
withoutNormalizing是“延迟”,不是“永不规范化” - Issue #2658:社区讨论 transaction 风格 API
5. 防死循环:shouldNormalize 与迭代上限
文件:packages/slate/src/core/should-normalize.ts
maxIterations = initialDirtyPathsLength * 42- 超过阈值抛错:
Could not completely normalize...
源码(节选):
const maxIterations = initialDirtyPathsLength * 42 // HACK: better way?
if (iteration > maxIterations) {
throw new Error(
`Could not completely normalize the editor after ${maxIterations} iterations! ...`
)
}这反映了一个现实权衡:
Normalization 是可扩展且递归触发的,必须有“刹车机制”防止错误 normalizer 造成无限循环。
相关讨论:
6. 初始值 normalize 的历史争议
是否自动对 initialValue 做全量 normalize 长期有争议:
- Issue #3465 提到“初始值不立即全量规范化”的行为影响
- 当前测试里可以看到
Editor.normalize(editor, { force: true })的显式路径(packages/slate/test/index.js)
可说:Slate 更倾向把 normalize 触发绑定到操作路径,而不是在所有时机做昂贵全量修复。
7. 关键得分点
Slate 的 normalize 是 multi-pass 修复系统。你写自定义 normalize 时应当“一次只修一个问题并 return”,让下一轮继续。
复杂命令要用withoutNormalizing包裹,避免中间态被提前修复。
同时要避免错误修复导致无限循环,Slate 通过迭代上限做保护。