核心数据模型与 Operation 系统
1. 数据模型:DOM-like,但不等于 DOM
Slate 文档是递归树:
Editor(根)Element(结构节点)Text(叶子文本节点)
定位与选择由三元组组成:
Path:树路径Point:{ path, offset }Range:{ anchor, focus }
这套抽象保证了“文档结构变化后仍可追踪位置”。
2. 为什么一切都落到 Operation
packages/slate/src/interfaces/operation.ts 注释写得非常直接:
把所有变化表示为 operation,才能容易实现 history、collaboration 等能力。
源码原文(节选):
ts
/**
* `Operation` objects define the low-level instructions that Slate editors use
* to apply changes to their internal state. Representing all changes as
* operations is what allows Slate editors to easily implement history,
* collaboration, and other features.
*/操作类型分三类:
- Node 操作:
insert_node/remove_node/move_node/set_node/split_node/merge_node - Text 操作:
insert_text/remove_text - Selection 操作:
set_selection
3. 执行与落盘:Transforms -> apply(op)
常见误区是把 Transforms 当作“底层”。
实际是:
Transforms是用户友好层(高层 API)- 最终都会变成 operation
- operation 统一交给
editor.apply(op)执行
因此可以说:Slate 不是命令类模式,而是“数据化命令(operation object)+ 统一执行器(apply)”。
4. 可逆性:Operation.inverse
Operation.inverse(op) 是历史和协作能力的关键基础:
insert_text<->remove_textinsert_node<->remove_nodeset_node交换properties/newPropertiesset_selection反转 old/new 选区move_node做路径修正,处理非 sibling move 的路径漂移
历史插件 slate-history 的 undo 正是基于这里逆序回放。
5. 源码证据
5.1 operation 类型定义和校验
文件:packages/slate/src/interfaces/operation.ts
Operation.isOperation做运行时结构校验Operation.isOperationList用于历史栈校验
5.2 apply 主干
文件:packages/slate/src/core/apply.ts
执行顺序非常重要:
- ref transform(PathRef/PointRef/RangeRef)
- dirty path 更新
Transforms.transform(editor, op)真正改文档- push
editor.operations - normalize
- flush
onChange
对应代码(节选):
ts
Transforms.transform(editor, op)
editor.operations.push(op)
Editor.normalize(editor, { operation: op })
if (!FLUSHING.get(editor)) {
Promise.resolve().then(() => {
editor.onChange({ operation: op })
editor.operations = []
})
}6. 与协作系统(OT/CRDT)的接口意义
Slate 本身不内建协作协议,但 operation 的数据化让你可以:
- 序列化并传输变化
- 回放远端变化
- 与外部 OT/CRDT 层对接
官方概念文档 docs/concepts/05-operations.md 也明确把这作为设计目标。
7. 相关 issue/讨论(设计依据)
- Issue #3741:
move_node信息与协作可逆性的关系 - Issue #5977:
isOperation与自定义 operation 的边界
8. 答法要点(30 秒)
Slate 的核心抽象不是 DOM 事件,而是 operation。
高层变换 API 最终都产出 operation,apply负责统一执行和规范化。
这种设计的收益是:历史、协作、回放、调试都能围绕同一数据模型开展,不会被 React 或浏览器事件耦死。