旧版的 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 章节了!
让我们养精蓄锐,迎接明天新的篇章吧!
<<: 【从零开始的Swift开发心路历程-Day21】简单介绍UIPickerView
>>: Day.25 Binary Search Tree III
Google i/o 2017 提到了Android TDD 的参考(https://develop...
Drawer 这个组件其实就是我们常用的 sidebar,继前一天的章节结合,就可以完成一个完整的应...
W10 预设只勾选SMB direct 所以要手动勾选SMB 1.0 *更新後可能会被取消勾选 ...
想请问各位, 如何读取 使用者传送的文件档案格式? 目前想要读取後 再存放到电脑指定资料夹中, 存放...
工作之余兴起开发side project的念头,几经思考後决定以Rust语言撰写一个基本的RISC-...