Day 18. slate × Immutable × Immer & slate

https://ithelp.ithome.com.tw/upload/images/20211003/20139359BwID3WAMQG.png

ImmutableJS v.s. ImmerJS


旧版的 Slate 选择 Immutable.js 这个套件去建立 Immutable object ,但这却造成了一些严重的缺点:

  • Immutable.js 的学习成本并不低,这造成了开发者需要事先具备使用 Immutable.js 的知识才能开始开发 Slate.js 。甚至旧版的 document 就直接跟开发者说『学习 Immutable.js 是一项非常值得的投资』呢!
  • Immutable.js 整个套件约为 15kb ,高过仅有 4kb 左右的 Immer.js 11kb 左右。
  • Immutable.js 引进了新的 Immutable 资料结构,而非纯粹的 JS Object ,这迫使开发者需要额外花费心力去处理 ImmutableJS 资料结构与 plain JS Object 之间的 transformation ,更遑论这件事所需要耗费的效能问题了。

这里再提供当时讨论 Immutable 套件转换的 Github issue 讨论串,因为顾及了旧版 Slate 的实作方式因此我们也能在里面看到各路 contributors 丢出当时所面临的 issues 以及各种 sample codes ,有兴趣的读者们可以上前观看历史的走向。


那麽在进入程序码与它提供的 api 前,我们首先来介绍一下 Immer.js 的运作概念。

Concept of Immer.js


https://ithelp.ithome.com.tw/upload/images/20211003/20139359lkXjik2DdW.png

如上图所示, 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

Immer - produce function


produce 是 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 掉。
  • 在提供原本的 Location value 、 Operation 、 options 後,经过一连串的运算回传更新过後的 Location value 。

Immer - createDraft / finishDraft function


另一个制造 draft state 的方式是使用 createDraftfinishDraft 这两个 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 更新,GeneralTransformstransform 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 绑定到 childrenselection 里。这里将主要的更新逻辑都封装在 applyToDraft function 里,这也是我们在 Operation 章节会着重介绍的部分。完成以後再透过 finishDraft 注销 Draft-State ,并回传更新过後的 childrenselection


针对 Immer.js 的使用就介绍到这边,当然除了上面所介绍的, Immer.js 还有提供许多针对不同情境所使用的 apis ,有兴趣的读者可以再前往它们的 官方文件 去查看。

Immutable 章节就到此为止,接下来我们就要进入重头戏之一—— Slate Operation 章节了!

让我们养精蓄锐,迎接明天新的篇章吧!


<<:  【从零开始的Swift开发心路历程-Day21】简单介绍UIPickerView

>>:  Day.25 Binary Search Tree III

谈谈TDD

Google i/o 2017 提到了Android TDD 的参考(https://develop...

Material UI in React [ Day 18 ] Drawer (侧边栏)

Drawer 这个组件其实就是我们常用的 sidebar,继前一天的章节结合,就可以完成一个完整的应...

W10 无法存取Server2003网路磁碟

W10 预设只勾选SMB direct 所以要手动勾选SMB 1.0 *更新後可能会被取消勾选 ...

LineBot 读取使用者档案格式

想请问各位, 如何读取 使用者传送的文件档案格式? 目前想要读取後 再存放到电脑指定资料夹中, 存放...

RISC-V on Rust 从零开始(1) - 安装 Rust 环境

工作之余兴起开发side project的念头,几经思考後决定以Rust语言撰写一个基本的RISC-...