Day 21. slate × Operation × Entrance

https://ithelp.ithome.com.tw/upload/images/20211006/20139359tfy5kdmIzs.png

上一篇我们介绍了 interfaces/operations.ts 里定义的 Operation types ,还没看过的读者们 传送门 在此~

接着从这一篇开始我们会从执行 Operation 的入口点,也就是位於 slate editor 的 core action : apply() 开始介绍,并深入进里头完成整个 Operations 流程的各种实作细节。

先来看一下 interfaces/editor.ts 里的 BaseEditor interface 是如何定义 apply method 的:

https://ithelp.ithome.com.tw/upload/images/20211006/201393590uR9aL6gx1.png

它的使用方式很基本,就是传入我们在前一篇文章介绍过的,符合 Operation type 规范的参数来执行指定的 Operation ,来看一下官方文件上提供的它的使用范例

editor.apply({
  type: 'insert_text',
  path: [0, 0],
  offset: 15,
  text: 'A new string of text to be inserted.',
})

editor.apply({
  type: 'remove_node',
  path: [0, 0],
  node: {
    text: 'A line of text!',
  },
})

editor.apply({
  type: 'set_selection',
  properties: {
    anchor: { path: [0, 0], offset: 0 },
  },
  newProperties: {
    anchor: { path: [0, 0], offset: 15 },
  },
})

作为所有 Operations 的入口点,我们在後续 Transforms 的章节也能时常看见它的身影,基本上我们可以说 Transforms 就是由各种 Operations 与条件判断所组成的 methods 。

接着我们就要来深入解析 Operation 的运作流程了!

Operation Process


就如同先前所介绍过的, slate editor 的实作由 create-editor.ts 里的 createEditor() 来完成,apply() method 也同样被实作在里头。

apply() 的流程主要分为以下几个部分:

  1. transform ref-type

    将目前 editor 中透过 WeakMap 标记的 Ref types 进行对应 Operation 的 Transform 转换,详细内容可以参考 Day 14 ,我们就不在这篇文章重复赘述了。

  2. set dirty-paths

    将每次执行 operation 『更动到的 path 』或『会受到影响的 path 』做肮脏标记,搭配後续第 4. 的 normalization 做 document model 正规化。

  3. transform editor

    实际搭配 Immer.js 执行 document model 资料更新的地方,也是整个 Operation 的主体。这边可能会有点误导读者,它与时常提及的 Transform method 指的不是同一件事,Transform method 是提供给开发者使用的 api ,一个 Transform method 里面通常含有复数个 Operations ,而这边的 transform 指的则是每次执行 Operation 时,更新 document model 所会呼叫的 function 。

    它被实作於 transforms/general.ts 里的 GeneralTransforms 里头,为了方便读者辨识,接下来笔者会以 Transform method 与 transform function 做区分。

  4. normalize editor

    与 dirty-path 搭配,负责 document model 资料正规化的地方。

  5. set FLUSHING

    我们有在 Day 10 时提到过这个流程,因为 slate 视 Operation 操作为原子操作 ,为了避免执行 Transform 时,复数个 Operations 导致 View-Layer 多余的画面渲染,也因此 slate 使用 Micro-task 搭配 FLUSHING WeakMap 解决这件事并在最後触发 onChange()

首先来上个 code 瞄个一眼, /** ... */ 的注释是笔者标上区分流程段落用的

apply: (op: Operation) => {
  /** Transform ref-type section */
  for (const ref of Editor.pathRefs(editor)) {
    PathRef.transform(ref, op)
  }

  for (const ref of Editor.pointRefs(editor)) {
    PointRef.transform(ref, op)
  }

  for (const ref of Editor.rangeRefs(editor)) {
    RangeRef.transform(ref, op)
  }

  /** Set dirty-paths section */
  const set = new Set()
  const dirtyPaths: Path[] = []

  const add = (path: Path | null) => {
    if (path) {
      const key = path.join(',')

      if (!set.has(key)) {
        set.add(key)
        dirtyPaths.push(path)
      }
    }
  }

  const oldDirtyPaths = DIRTY_PATHS.get(editor) || []
  const newDirtyPaths = getDirtyPaths(op)

  for (const path of oldDirtyPaths) {
    const newPath = Path.transform(path, op)
    add(newPath)
  }

  for (const path of newDirtyPaths) {
    add(path)
  }

  DIRTY_PATHS.set(editor, dirtyPaths)

  /** Transform editor section */
  Transforms.transform(editor, op)
  editor.operations.push(op)

  /** Normalize editor section */
  Editor.normalize(editor)

  // Clear any formats applied to the cursor if the selection changes.
  if (op.type === 'set_selection') {
    editor.marks = null
  }

  /** FLUSHING section */
  if (!FLUSHING.get(editor)) {
    FLUSHING.set(editor, true)

    Promise.resolve().then(() => {
      FLUSHING.set(editor, false)
      editor.onChange()
      editor.operations = []
    })
  }
},

我们先来看看第 5. 的 FLUSHING section 。

// weak-maps.ts
export const FLUSHING: WeakMap<Editor, boolean> = new WeakMap()

//create-editor.ts
if (!FLUSHING.get(editor)) {
  FLUSHING.set(editor, true)

  Promise.resolve().then(() => {
    FLUSHING.set(editor, false)
    editor.onChange()
    editor.operations = []
  })
}

slate 首先制作了一个 FLUSHING 的 WeakMap variable ,作为 Editor node 的扩充标记,记录每个当下的 Editor node 是否正在执行一连串的 Operation 更新

每次的 Operation 都会先判断 EditorFLUSHING value 是否为 falsy ,若为 falsy 则代表这是整串 Operation queue 中的第一个 Operation 。

将 FLUSHING value 设为 true 後接着将整串 Operations 最後要执行的行为封装进 Promise .then() 的 callback 中,成为 Micro-task 推送到 Job Queue 。

因为整个 Operation 里执行的内容皆为 Synchronous 的,因此能确定这组 Micro-task 会在正确的时间点被执行,最後再将 FLUSHING value 设为 false → 执行 onChange() → 清空 operation list 。

这里的 code 行数短短不到 10 行,却完美地控制了编辑器渲染层的 re-render 时机点,因为是单纯透过一组 WeakMap 来处理整个判断的,所以我们也能透过操作它来得到更多的弹性(只不过 Slate 目前没有把 WeakMap export 出来让开发者使用,只能等作者把相关的 PR merge 起来或是先自己改罗XD)


有关於 Promise 的 Micro-task 相关的内容,笔者这边提供给不清楚的读者们一篇文章上前查阅

JS 原力觉醒- Macrotask 与 MicroTask

紧接着下一篇我们要长驱直入第 3. ,一口气了解 Operation 的本体 transform function 里头是如何运作的。

一样下篇文章见罗!请拭目以待~


<<:  Day 21 : 笔记篇 08 — 数位笔记太多很凌乱怎麽办?使用 MOC 架构有系统地管理数百则的数位笔记

>>:  Day 24 - 结构化思维的解构训练真的很重要!!!

[前端暴龙机,Vue2.x 进化 Vue3 ] Day5. Vue的起手开发

接下来,开始看看如何着手进行 Vue 的开发吧 这边都是透过最原始、最简单的网页开发模式进行,所以不...

【设计+切版30天实作】|Day5 - 做出3栏式「痛点」设计

设计大纲 早安!今天来设计痛点,这边我想要做三栏式,列出三个他们目前主要可能会遇到的问题,再加上图片...

Day 7. 关於.NET新手遇到问题,我是这样建议

新手在刚开始学习时,在工作上往往会遇到许多的困难,而在这边我有一些建议可以给新手 1. 学习怎麽Go...

D-18. SQL & NoSQL、SQL injection、primary key & foreign key

SQL && NoSQL SQL Structured Query Language...

[Day27] Room | 官方目前推荐不使用Sqlite

参考连结 先在build.gradle中加入以下这段 def room_version = &quo...