iT邦幫忙

2021 iThome 鐵人賽

DAY 19
0
Modern Web

深入 slate.js x 一起打造專屬的富文字編輯器吧!系列 第 19

Day 19. slate × Operation × WeakMap

https://ithelp.ithome.com.tw/upload/images/20211004/20139359TwQRUR0Kp7.png

在正式開始介紹 Operation 的內容之前,先讓我們花一點篇幅來介紹一下『 WeakMap 』這個感覺有點偏冷門的主題。

即便對它不熟悉的讀者們,第一眼看到它的名字應該也能馬上與 JS ES6 的 Map 聯想在一塊兒。

它們其實是非常相似的東西,使用與操作上的邏輯也差不多,重點就是差在那個『 Weak 』字而已。

What & Why WeakMap


預設讀者對 Map 有一定基礎的認識下,讓我們直接列舉出 Map 與 WeakMap 兩者之間的差異:

  • Map instance 的 key 可以是任何 type ,但 WeakMap instance 中的 key 只能是 object type

  • WeakMap instance 裡面引用的 key object ,必須在其外部存在其他的 references

    當外部沒有其他引用 WeakMap 裡特定 key object 的 reference 時,該項 key value pair 會被 Garbage Collection 清除出 WeakMap 裡。

    https://ithelp.ithome.com.tw/upload/images/20211004/20139359LfLZ4wJWRZ.png

  • WeakMap instance 是 Non-enumerable (不可列舉的)

    在 Javascript 裡提供的 Map api 包含: entriesforEachkeysvalues 這四種 methods 讓開發者能夠列舉一個 Map instance 裡的資料,但在 WeakMap api 裡卻不存在著這些 methods 。

了解了 WeakMap 與他的兄長之間的差異以後,我們當然也要懂 WeakMap 存在的價值,依照 MDN 對 WeakMap 的解釋我們可以列出兩點使用 『 Map 』 時會遇到的問題:

  1. 當我們使用 set api 去新增新的 key value pair 進 Map 裡頭時, Map 會將這個新安插的資料 push 進整個 Map instance 資料的最尾端,這使得我們使用 get methods 查詢 Map instance 裡的資料時,都會因為需要遍歷整個 Map list 而導致 O( n ) 的時間複雜度( n 是 key value pair 的個數)。
  2. 過度使用很容易造成 memory leak ,因為 Map 會無期限地確保對 instance 內所有 key 與 value 值的 references ,這導致了裡頭的 key 與 value 都不會被 GC 掉,就算在外部已經完全沒有 reference 了也一樣。

一個最直接理解 WeakMap 的方式就是:它提供給開發者一個『延伸物件儲存資料的方式』。很多時候我們在設計與封裝完一組 Object ,或甚至它的來源是從第三方套件取得的資料時,會希望能延伸出這組 Object 關聯到的資料。此時就是 WeakMap 登場的時機了!

Slate Document tree 的特性使它尤其適合搭配 WeakMap 的使用,因為 tree 裡頭的任意一個節點與路徑都有機會因為編輯器的更新而被當作過期的資料取消對它的關聯,為了讓它們能在正確的時機點被順利地 GC 掉,透過 WeakMap 來做擴充標記就是一個很好的選擇。

接著就來看看 Slate 裡頭有哪些使用 WeakMaps 的情境吧!

The WeakMaps In Slate


首先是以『 Cache 』的方式來使用它,這同時也是 WeakMap 最普遍被使用的方式了,基本上只要是有使用 WeakMap 的地方幾乎都能看到這種用法的影子。

這邊我們要舉的範例是:interfaces/editor.ts Editor 裡的 isEditor

isEditor 是 Editor 裡的 method apis 裡用來判斷傳入的 value 是否為 Editor 。

  1. 它首先從 IS_EDITOR_CACHE 的 WeakMap instance 中尋找 value key 的 value
  2. 如果回傳的資料,也就是 cachedIsEditor 不為 undefined 則代表已對這個 value 做過緩存,直接 return 緩存的結果即可
  3. 若為 undefined 則重新進行一次 type check 並將結果( isEditor )存入以 value 作為 key 的 IS_EDITOR_CACHE WeakMap instance 裡。

當我們將 value 的結果存入緩存後,外部對於 value 這個 object 的 reference 遭到拔除以後, IS_EDITOR_CACHE 也會將 value 的 key value pair 丟給 Garbage Collection 做清理。

const IS_EDITOR_CACHE = new WeakMap<object, boolean>()

export const Editor: EditorInterface = {
	// ...,
	isEditor(value: any): value is Editor {
		// ...
		const cachedIsEditor = IS_EDITOR_CACHE.get(value)
    if (cachedIsEditor !== undefined) {
      return cachedIsEditor
    }
		const isEditor = // Editor type checks
		IS_EDITOR_CACHE.set(value, isEditor)
    return isEditor
	},
}

再來就是我們在介紹 Ref concept 時也有提到過的『擴充標記』了, Slate 將這種類型的功能統一整理在一個叫 weak-maps.ts 的 file 裡。在 slate-react package 裡我們也能看到 weak-maps file 裡統整了一系列將 Slate nodes 關聯到 HTML DOM nodes 的 WeakMap list :

/**
 * Weak maps that allow us to go between Slate nodes and DOM nodes. These
 * are used to resolve DOM event-related logic into Slate actions.
 */
export const EDITOR_TO_WINDOW: WeakMap<Editor, Window> = new WeakMap()
export const EDITOR_TO_ELEMENT: WeakMap<Editor, HTMLElement> = new WeakMap()
export const EDITOR_TO_PLACEHOLDER: WeakMap<Editor, string> = new WeakMap()
export const ELEMENT_TO_NODE: WeakMap<HTMLElement, Node> = new WeakMap()
export const KEY_TO_ELEMENT: WeakMap<Key, HTMLElement> = new WeakMap()
export const NODE_TO_ELEMENT: WeakMap<Node, HTMLElement> = new WeakMap()
export const NODE_TO_KEY: WeakMap<Node, Key> = new WeakMap()

回到 slate package 的 weak-maps.ts file 也能看到一系列對於 Editor 的擴充標記,除了在 Day 14 提到過的 REFS WeakMap 之外,其餘全部都被 Slate 應用在 Operation 相關的功能

export const DIRTY_PATHS: WeakMap<Editor, Path[]> = new WeakMap()
export const FLUSHING: WeakMap<Editor, boolean> = new WeakMap()
export const NORMALIZING: WeakMap<Editor, boolean> = new WeakMap()
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()

如果是位於 tree 裡頭的節點諸如 ElementText 透過 WeakMaps 進行標記我覺得合理,畢竟它們就是被設計成會被頻繁更改的。但在 slate package 裡 WeakMaps 的 key 存放的都是 Editor ,它身為整個 tree 的根節點應該不太需要考慮 GC 的問題吧?怎麼還需要刻意透過 WeakMaps 來存取它的資料呢?


如果有這樣的想法的話就代表思考的方向已經違反了 Slate 的 principles 之一,也就是不會去預測開發者對編輯器億的使用情境了。

確實位於 Slate node tree 中的根節點會是 Editor 沒錯,但我們卻不應該去預測使用者不會在子層再放入另一個 Editor 作為 child node 。

Slate 視一切 Node 為『可被 Garbage Collect 的 unique object reference』,而 Node type 的定義為 Node = Editor | Element | Text ,在這樣的前提下 Editor 也應當要被視為了可被 GC 的 unique object 。


介紹完 WeakMaps 後我們接著就要正式開始介紹 Operations 了!下一篇我們一樣會從 Interface 篇章被我們略過的 interfaces/operation.ts 開始介紹起。

那麼我們就明天再見囉~


上一篇
Day 18. slate × Immutable × Immer & slate
下一篇
Day 20. slate × Operation × Interface
系列文
深入 slate.js x 一起打造專屬的富文字編輯器吧!30

尚未有邦友留言

立即登入留言