相信有 React 开发经验的读者们对 Ref 这个词一定不陌生。
其实 slate 里头的 Ref concept 与 React Ref 非常相似,同样都是用来指向 Document ( HTML Document / Slate Document ) 中的某一个 Node ( DOM Node / Slate Node ),并持续追踪这个节点的资料更新。
在开始介绍之前为了 让文章篇幅看起来长一点 能更全面地认识它,我们先来追朔一下整个概念的历史由来。
slate 里头的 refs concept 是由作者之一的 ianstormtaylor 在 github issue 上发问索取想法的。
旧版的 slate 中,tree 里头所有的 Node 都带有一个 auto-incrementing 的 key
属性,而 Point
除了我们前文提到的 path
与 offset
属性之外还有一个 key
属性对应到指定 Node 的 key
值。
Point({
key: String,
path: List,
offset: Number,
})
path
与 key
这两个属性提供给无论开发者或是 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 的讨论串,有兴趣的读者可以上前去观赏它的演化史。
在 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 集合。
接着来看一下各个 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
资料的流程。
为什麽会有『更新
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 是如何与它做搭配的。
我们一样明天见罗~
>>: 更新Android Studio Arctic Fox | 2020.3.1与android X 与相关开发环境升级
今天,我们要来作Alpine Linux的initramfs bootstrapping。 在近代的...
前言 我们在前一天开发完成了套件,那麽就试着来上架ㄅ 。 可以查看 官方文件,肯定讲的比我清楚哈哈(...
<if> 条件控制 <if> 元素根据 test 属性中的评估值决定其下的元...
实务上,因应不同的开发阶段,应用程序会运行在开发环境 (Develop Environment)、预...
待更新待更新待更新待更新待更新待更新待更新待更新待更新待更新待更新待更新待更新待更新待更新待更新待更...