富文本编辑.Slate源码分析1.宏观.极短极简

266 阅读4分钟

Slate作为高可定制的富文本框架被国内越来越多开发厂商所使用,我在自研产品技术选型用Slate后,这次打算静下心来读读它的源码,在掘金贴出来是想请大家斧正,共同进步。Slate的Core架构写的是非常清晰的,希望我能为大家说明白。 image.png


定义结构

树形结构, 成员老三样
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 逻辑

这下面两段代码比较精华(代码稍做修改),能分析出下面的关健信息:

  1. onChange中访问editor.operation将会包含transform、normalizing
  2. transform、normalizing、onChange中产生的所有Op,都会压入同一Undo节点中
  3. 一个用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也会更方便些。不过我自觉还是没细看代码,有错误不对需补充的地方还希望大家不吝指正。谢谢!