Slate作为高可定制的富文本框架被国内越来越多开发厂商所使用,我在自研产品技术选型用Slate后,这次打算静下心来读读它的源码,在掘金贴出来是想请大家斧正,共同进步。Slate的Core架构写的是非常清晰的,希望我能为大家说明白。
定义结构
树形结构, 成员老三样
Text - 叶节点 内联元素
Element - 树叉节点 块元素
Editor - 根节点
寻址, 同样三种
Path = number[]
editor下第一个ele的path是[0],后一个兄弟就是[1],第一个ele下的第一个ele儿子的path是[0,0],依此类推
Point = { path: Path offset: number }
多了个offset来能指示是叶节点上第几个文字
Rang = { anchor: Point focus: Point }
用来表示一段连续区间 值得注意的是:调用slate的函数时不能传入无效path(会抛异常),其实slate提供了很丰富的辅助函数,寻址计算是不要自己写的,稍稍耐心些翻一下文档或源码,用用还是非常方便的,如下面这一组可以了解一下。多了个辅参edge不赘述了。
start(editor: Editor, at: Location): Point {
return Editor.point(editor, at, { edge: "start" });
},
end(editor: Editor, at: Location): Point {
return Editor.point(editor, at, { edge: "end" });
},
edges(editor: Editor, at: Location): [Point, Point] {
return [Editor.start(editor, at), Editor.end(editor, at)];
},
编辑结构
Operation - 原子操作
最基本的更改结构的操作函数,也只有9个, 修改Node节点也就是块节点的6个,修改文字的2个,修改选取(也可以简单理解为光标位置)1个。通过调用Editor.apply(op)执行.
解释Node中比较费解的Op:
- Merge - 在某行头部敲Backspace,并到前一行尾部,也就是去回车
- Split - 在某行中间敲回车,一行变两行
- Move - 这个好像用户交互很难触发得了,调用slate提供的功能函数liftNodes会触发
export type NodeOperation =
| InsertNodeOperation
| MergeNodeOperation
| MoveNodeOperation
| RemoveNodeOperation
| SetNodeOperation
| SplitNodeOperation
export type TextOperation = InsertTextOperation | RemoveTextOperation
export type SelectionOperation = SetSelectionOperation
Transform - 事务
组合上面9个达成一定的目的 Transform直译应该说是变换,意思大概想把逻辑计算模型照着矩阵变换可叠加方向上去整,可现在的代码中我还没看到两个Op进行合成后再使用,而是一个一个的在调apply(op)执行,加上Slate算是默认支持Undo/Redo(虽然包在Core外的Pkg中),所以觉的是不是叫着事务更贴切些。
Undo/Redo 逻辑
这下面两段代码比较精华(代码稍做修改),能分析出下面的关健信息:
- onChange中访问editor.operation将会包含transform、normalizing
- transform、normalizing、onChange中产生的所有Op,都会压入同一Undo节点中
- 一个用Promise.resolve().then创建微任务的精典场景。
apply: (op: Operation) => {
...
/**是否将当前op压入已有Undo的最后一个节点中,换句话说就是和前面的op逻辑上是同一个事务 */
let merge = false;
/**如果存在undo节点*/
if (lastUndo) {
/**😱这里有些潜规则,也就是当前宏任务有多个Op时,会合并在一个Undo节点中*/
if (editor.operations.length !== 0) {
merge = true;
} else {
merge = shouldMerge(curOp, lastUndoOp) || overwrite;
}
}
apply: (op: Operation) => {
...
editor.operations.push(op)
Editor.normalize(editor)
if (!FLUSHING.get(editor)) {
FLUSHING.set(editor, true)
/**😱通过Promise.resolve().then生成一个微任务,这个任务在当前代码块完成后执行*/
Promise.resolve().then(() => {
FLUSHING.set(editor, false)
editor.onChange()
editor.operations = []
})
}
微任务机制
- 就相当于提供滞后执行机制,一个宏任务(当前脚本块) 执行过程中可以生成滞后但会在渲染前执行的任务,这些任务称为微任务。
- 所有的微任务会在当前脚块所有脚本运行完成后渲染前执行
- 宏任务:script主代码块、setTimeout 、setInterval 、nodejs的setImmediate...
- nodejs中微任务的优先级:process.nextTick >new Promise().then(回调) > MutationObserver 参考: 事件循环、宏任务、微任务一网打尽(附超多经典面试题)
约束结构
Slate在计算时还是需依赖树结构遵守一定的规则,从而简化计算复杂度,使计算过程可实现可控。
规则详见: rain120.github.io/athena/zh/s…
slate成员很虚心的向用户咨询嫌不嫌规则复杂,爱了爱了😘 敬礼!
上方贴出的apply()代码段中有这样一句Editor.normalize(editor),它表达了slate认为每次op都可能会造成结构变形,都会试着去修正的愿望。
但事务中是由一系列相互关联的Op组成,其中每个Op执行完后并无必要且不应去修正,只有这个事务整体执行完成才应去修正结构。
所以查看Slate源码,就会看到一般事务中所有的apply(op)都会包在箭头函数中交给withoutNormalizing这个函数去执行。
withoutNormalizing
本函数通过调用setNormalizing为(false)让箭头函数中每次Apply(Op)时都不要进行规范化,而在箭头函数执行完后再调用一次规范化。
withoutNormalizing(editor: Editor, fn: () => void): void {
const value = Editor.isNormalizing(editor);
Editor.setNormalizing(editor, false);
try {
fn();
} finally {
Editor.setNormalizing(editor, value);
}
Editor.normalize(editor);
}
normalize(
editor: Editor,
options: {
force?: boolean;
} = {}
): void {
...
if (!Editor.isNormalizing(editor)) {
return;
}
到这里,Slate核心的宏观介绍我这儿算完了,相信大家有了宏观视角后再去分析源码使用Slate也会更方便些。不过我自觉还是没细看代码,有错误不对需补充的地方还希望大家不吝指正。谢谢!