相信有 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 是如何與它做搭配的。
我們一樣明天見囉~