Day 14. slate × Interfaces × Ref

https://ithelp.ithome.com.tw/upload/images/20210929/20139359N6DVCQNRv8.png

相信有 React 开发经验的读者们对 Ref 这个词一定不陌生。

其实 slate 里头的 Ref concept 与 React Ref 非常相似,同样都是用来指向 Document ( HTML Document / Slate Document ) 中的某一个 Node ( DOM Node / Slate Node ),并持续追踪这个节点的资料更新。

在开始介绍之前为了 让文章篇幅看起来长一点 能更全面地认识它,我们先来追朔一下整个概念的历史由来。

Concept History


slate 里头的 refs concept 是由作者之一的 ianstormtaylor 在 github issue 上发问索取想法的。

旧版的 slate 中,tree 里头所有的 Node 都带有一个 auto-incrementing 的 key 属性,而 Point 除了我们前文提到的 pathoffset 属性之外还有一个 key 属性对应到指定 Node 的 key 值。

Point({
  key: String,
  path: List,
  offset: Number,
})
使用上跟新版 slate 比较不一样,但因为不是本篇的重点就先略过解释了,概念基本上是一样的。

pathkey 这两个属性提供给无论开发者或是 slate 本身得以 reference 到 slate tree 的指定节点,但後者因为就只是个 unique string 而已,要想透过它来查找对应的节点就必须要完整遍历整个 slate tree ,而不像 path 只要寻着阵列里头的数字,很快就能抵达指定节点。

但我们却无法透过 path 的 value 来持续追踪指定节点,因为经过 insert 、 remove 等操作时,同个节点的 path 是有可能会更动,甚至被作废的。

也因此作者提出了 refs concept ,起初是为了让 Point 透过『非』 Document 查找的方式,而是有个 mutable object 能持续追踪它的资料变化,同时能够透过 unref method 让它能够被 Garbage collect 掉,经过了一连串的讨论演化以後才成为了我们现在看到的 ref interface 的形式。

一样附上 Github issue 的讨论串,有兴趣的读者可以上前去观赏它的演化史。


Slate Refs


在 weak-maps.ts 里面我们会看到三组 *_REFS 的 WeakMap 变数:

// weak-map.ts

export const PATH_REFS: WeakMap<Editor, Set<PathRef>> = new WeakMap()
export const POINT_REFS: WeakMap<Editor, Set<PointRef>> = new WeakMap()
export const RANGE_REFS: WeakMap<Editor, Set<RangeRef>> = new WeakMap()

这些 WeakMaps 里纪录着 editor 里头所有的 Ref 集合。

关於 WeakMaps 我们之後会特别拉出一篇文章来介绍它

接着来看一下各个 ref 的 interfaces :

export interface PathRef {
  current: Path | null
  affinity: 'forward' | 'backward' | null
  unref(): Path | null
}

export interface PointRef {
  current: Point | null
  affinity: 'forward' | 'backward' | null
  unref(): Point | null
}

export interface RangeRef {
  current: Range | null
  affinity: 'forward' | 'backward' | 'outward' | 'inward' | null
  unref(): Range | null
}
  • current

    就跟 React Ref 的 current property 一样,储存着 Slate Node 的资料。

  • affinity

    这项资料会在每次执行 Operation 时提供给各个对应的 transform method 的 affinity option 。

  • unref

    将这项 ref 彻底删除到可以被 GC 的程度。

所有与 Ref 相关的实作都被写在 editor.ts 的 Editor method apis 里面,例如:

  • 取得 WeakMap 里头的 editor ref set :

    export interface EditorInterface {
    	pathRefs: (editor: Editor) => Set<PathRef>
    	pointRefs: (editor: Editor) => Set<PointRef>
    	rangeRefs: (editor: Editor) => Set<RangeRef>
    }
    
  • 制作 ref 并绑定到对应的 WeakMap set

    export interface EditorInterface {
    	pathRef: (
        editor: Editor,
        path: Path,
        options?: {
          affinity?: 'backward' | 'forward' | null
        }
      ) => PathRef
    	pointRef: (
        editor: Editor,
        point: Point,
        options?: {
          affinity?: 'backward' | 'forward' | null
        }
      ) => PointRef
    	rangeRef: (
        editor: Editor,
        range: Range,
        options?: {
          affinity?: 'backward' | 'forward' | 'outward' | 'inward' | null
        }
      ) => RangeRef
    }
    

因为实作的内容大同小异,顶多带的参数不太一样而已,我们就拿 pathRef 当作范例来介绍吧。

pathRef(
  editor: Editor,
  path: Path,
  options: {
    affinity?: 'backward' | 'forward' | null
  } = {}
): PathRef {
  const { affinity = 'forward' } = options
  const ref: PathRef = {
    current: path,
    affinity,
    unref() {
      const { current } = ref
      const pathRefs = Editor.pathRefs(editor)
      pathRefs.delete(ref)
      ref.current = null
      return current
    },
  }

  const refs = Editor.pathRefs(editor)
  refs.add(ref)
  return ref
},

code 并不复杂,就是把传入的参数摆进 ref 变数,同时实作 unref method :执行 pathRefs method 回传的 Set 的 delete method 并把 current 设为 null 。再把定义好的 ref 加进 Set 里面。

pathRefs method 里头做的事也就是单纯回传 Set 以及初始化而已。

pathRefs(editor: Editor): Set<PathRef> {
  let refs = PATH_REFS.get(editor)

  if (!refs) {
    refs = new Set()
    PATH_REFS.set(editor, refs)
  }

  return refs
},

介绍完它的 interface 以及实作以後我们来看一下 Ref 更新 current 资料的流程。

Ref Transform


为什麽会有『更新 current 资料』的必要存在啊? Ref 不是就直接指向 Node 的位置了吗?这样当 Node 被更新了以後 Ref 就应该跟着被更新了吧?


这部分就会牵涉到之後 Immutable 的章节内容了,这边先简单介绍原因:

这是因为所有的 Location type 的更新操作都会经过 Immer 的包装,更新後的变数是指向新的位置而非旧的,也因此 Ref current 的资料指向的位置与 Slate 真实的 Node 指向的位置并不一样。

所以为了保持它们之间资料的一致性所以我们需要在每次的 Operation 都对所有的 refs 执行一次相同的 Operation 。

Ref 的资料更新相关的内容是放在 create-editor.ts 里 editor 的 apply method ,关於 apply 的详细介绍我们留到之後的 Operation 篇章,这边读者先知道这个 method 是所有 Operations 的入口,也就是执行任意 Operation 的起点就可以了。

在 method 的最一开始我们就能看到三组 for loop 去执行 *Ref.transform method :

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)
}

*Ref.transform method 里做的事也不多,就是将传入的 ref 资料传给对应 concept 的 transform method api 并重新绑定或 unref 回传的资料。

我们一样拿 PathRef 的 transform method 当作范例:

transform(ref: PathRef, op: Operation): void {
  const { current, affinity } = ref

  if (current == null) {
    return
  }

  const path = Path.transform(current, op, { affinity })
  ref.current = path

  if (path == null) {
    ref.unref()
  }
},

Ref 对於 slate 而言是一个 lower level interface ,我们可以在许多 Transform methods 里面看到使用 Ref 的踪迹,能更全面地了解这项概念对我们之後要深入探讨 Transforms 也会有不小的帮助。

那麽今天对於 Ref 的概念我们就介绍到这边,下一篇就要来收回在之前的文章里挖的坑,完整地介绍一轮 JS iterate 以及 slate 的 EntryType 是如何与它做搭配的。

我们一样明天见罗~


<<:  最短路径问题 (3)

>>:  更新Android Studio Arctic Fox | 2020.3.1与android X 与相关开发环境升级

Alpine Linux Porting (一点三?)

今天,我们要来作Alpine Linux的initramfs bootstrapping。 在近代的...

【第二九天 - Flutter 开发套件之旅(下)】

前言 我们在前一天开发完成了套件,那麽就试着来上架ㄅ 。 可以查看 官方文件,肯定讲的比我清楚哈哈(...

Shadow Element:条件控制元件的创建、消灭

<if> 条件控制 <if> 元素根据 test 属性中的评估值决定其下的元...

第 30 型 - 环境配置与建构 (Build)

实务上,因应不同的开发阶段,应用程序会运行在开发环境 (Develop Environment)、预...

待更新

待更新待更新待更新待更新待更新待更新待更新待更新待更新待更新待更新待更新待更新待更新待更新待更新待更...