舊版的 Slate 選擇 Immutable.js 這個套件去建立 Immutable object ,但這卻造成了一些嚴重的缺點:
這裏再提供當時討論 Immutable 套件轉換的 Github issue 討論串,因為顧及了舊版 Slate 的實作方式因此我們也能在裡面看到各路 contributors 丟出當時所面臨的 issues 以及各種 sample codes ,有興趣的讀者們可以上前觀看歷史的走向。
那麼在進入程式碼與它提供的 api 前,我們首先來介紹一下 Immer.js 的運作概念。
如上圖所示, Immer 的使用上都圍繞在一個位於 Current state 與 Next state 之間,名叫 Draft state 的灰色地帶。為了讓開發者在操作時與操作一般 Mutable Object 時一樣直觀,它在 Draft state 裡提供一個 Current state 的 proxy object 讓開發者直接對這個 proxy object 做資料修改。
等到修改結束後, Immer 會再比對 Draft state 裡的修改內容,並將結果輸出成 Next state 。
不像 Immutable.js ,所有 state 資料的 CRUD 都是透過它提供給你的 api 進行,需要花相對多的時間去學習各個 api 的使用情境。 Immer 提供的更像是一張印有原始資料的稿紙,待開發者確定並將稿紙上的修改送出後才執行真正的 Immutable state change 。
來看一下 Immer 的官網是如何形容自己的:
Using Immer is like having a personal assistant. The assistant takes a letter (the current state) and gives you a copy (draft) to jot changes onto. Once you are done, the assistant will take your draft and produce the real immutable, final letter for you (the next state).
再來我們就要進到 code 與 Immer.js 提供的 api 的部分了,因為篇幅問題所以我們只會把重點圍繞在 slate 的使用範圍之下去延伸,包含了最基本的 produce
以及 createDraft
/ finishDraft
。
produce
functionproduce
是 Immer.js 提供,達成上半部所描述的運作理念,最基本最核心的 api 了。我們先一起來看一下它的 formula :
produce(currentState, recipe: (draftState) => void): nextState
在第一個參數放入要修改的 state ,它會提供 draft state 給第二個 callback function 參數供開發者使用,最後回傳修改完成的 next state 。
我們能以一般函式的使用方式使用它:
import produce from "immer"
const baseState = [
{
title: "Learn TypeScript",
done: true
},
{
title: "Try Immer",
done: false
}
]
const nextState = produce(baseState, draftState => {
draftState.push({title: "Tweet about it"})
draftState[1].done = true
})
也能以 Curry function 的方式來使用它:
import produce from "immer"
function toggleTodo(state, id) {
return produce(state, draft => {
const todo = draft.find(todo => todo.id === id)
todo.done = !todo.done
})
}
const baseState = [
{
id: "JavaScript",
title: "Learn TypeScript",
done: true
},
{
id: "Immer",
title: "Try Immer",
done: false
}
]
const nextState = toggleTodo(baseState, "Immer")
上面都是官網提供的範例,從丟進 produce
的 callback function 裡可以看到我們修改 draft state 的方式就跟一般我們在使用 JS 的 Object type data 一模一樣,它讓我們能在保有 Immutable 特性的優點下用自己最習慣的方式與 data 互動。
而在 Slate 裡,主要使用 produce
method 的地方就在所有 Location
types 提供的 transform
method :
// interfaces/path.ts
/**
* Transform a path by an operation.
*/
transform(
path: Path,
operation: Operation,
options: { affinity?: 'forward' | 'backward' | null } = {}
): Path | null {
return produce(path, p => {
// ... Path.transform implementation
});
}
// interfaces/point.ts
/**
* Transform a point by an operation.
*/
transform(
point: Point,
op: Operation,
options: { affinity?: 'forward' | 'backward' | null } = {}
): Point | null {
return produce(point, p => {
// ... Point.transform implementation
});
}
// interfaces/range.ts
/**
* Transform a range by an operation.
*/
transform(
range: Range,
op: Operation,
options: {
affinity?: 'forward' | 'backward' | 'outward' | 'inward' | null
} = {}
): Range | null {
// ... Statement control
return produce(range, r => {
// ... Range.transform implementation
})
}
這些 transform
methods 被使用於整個 editor state 更新的最底層,只要與『節點的路徑更新』相關的功能情境出現時,就會呼叫這些 methods 。
它們負責的工作有兩個:
produce
確保每個存在於 Slate editor 裡的 Locations 皆為 unique 且 immutable 的,讓那些沒有被 reference 到的 Locations 被自然地 GC 掉。createDraft
/ finishDraft
function另一個製造 draft state 的方式是使用 createDraft
與 finishDraft
這兩個 api ,有別於 produce
是提供一組 object proxy 在傳入的 callback function 中使用,createDraft
則是會回傳丟入的 current state 的 draft state 供開發者 mutate ,一樣記錄在這期間做的所有操作直到呼叫 finishDraft
回傳下一組 immutable state 並註銷原本的 draft state 。讓開發者不用每次需要使用 draft state 時都要重新建立一組新的 function 。
我們直接拿官方提供的範例來輔助說明:
import {createDraft, finishDraft} from "immer"
const user = {
name: "michel",
todos: []
}
const draft = createDraft(user)
draft.todos = await (await window.fetch("http://host/" + draft.name)).json()
const loadedUser = finishDraft(draft)
可能讀者會認為這組 methods 是專門拿來處理非同步相關情境的,其實不然,開發者也能提供非同步函式給 produce
const user = {
name: "michel",
todos: []
}
const loadedUser = await produce(user, async function(draft) {
draft.todos = await (await window.fetch("http://host/" + draft.name)).json()
})
最後 Slate 使用這組 methods 的情境是在 Operation 實際處理 editor state 更新,GeneralTransforms
的 transform
method 裡,這部分的詳細介紹我們放在 Operation 的章節裡,讀者目前先了解這個 method 是所有的 Operations 實際執行『更新 editor state 』時會呼叫的 method 就好,我們先把重點聚焦在 createDraft
/ finishDraft
method 的使用上:
export const GeneralTransforms: GeneralTransforms = {
/**
* Transform the editor by an operation.
*/
transform(editor: Editor, op: Operation): void {
editor.children = createDraft(editor.children)
let selection = editor.selection && createDraft(editor.selection)
try {
selection = applyToDraft(editor, selection, op)
} finally {
editor.children = finishDraft(editor.children)
if (selection) {
editor.selection = isDraft(selection)
? (finishDraft(selection) as Range)
: selection
} else {
editor.selection = null
}
}
},
}
首先透過 createDraft
將 Draft-State 綁定到 children
與 selection
裡。這裡將主要的更新邏輯都封裝在 applyToDraft
function 裡,這也是我們在 Operation 章節會著重介紹的部分。完成以後再透過 finishDraft
註銷 Draft-State ,並回傳更新過後的 children
與 selection
。
針對 Immer.js 的使用就介紹到這邊,當然除了上面所介紹的, Immer.js 還有提供許多針對不同情境所使用的 apis ,有興趣的讀者可以再前往它們的 官方文件 去查看。
Immutable 章節就到此為止,接下來我們就要進入重頭戲之一—— Slate Operation 章節了!
讓我們養精蓄銳,迎接明天新的篇章吧!