iT邦幫忙

2021 iThome 鐵人賽

DAY 17
0
Modern Web

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

Day 17. slate × Immutable

https://ithelp.ithome.com.tw/upload/images/20211002/20139359Mvr60ZvnP1.png

接著我們要進入到 slate 的下一個重點章節: Immutability 。

雖然這已經算是一個老生常談的主題了,但還是先讓我們稍微花點篇幅討論一下:什麼是 Immutable ?為什麼我們要使用它?我們又能夠如何實現它?

What


Immutable (不變的):其實在各種程式語言裡我們都能看到這個概念的蹤跡,在 Google 直接下關鍵字除了 Google 跟劍橋辭典貼心幫我們翻譯成中文以外,剩下的就是滿坑滿谷的相關文章,又是 C 又是 Python 又是 Javascript 的,可見它的重要性。

當然我們始終都是圍繞在 Javascript ,那麼既然都要討論了我們就從 JS 最基本的 Primitive types 開始討論起順便複習複習吧!

你知我知,對 JS 有基本認識的人都知,基本 JS 的資料型別可以分為兩類:

  • Primitive type 原始型別
  • Object type 物件

除了 Boolean 、 Null 、 Undefined 、 Number 、 BigInt 、 String 、 Symbol 這七種被歸類為 Primitive type 以外,其餘一切皆為 Object 。

Primitive 與 Object type 有兩種主要相對的特性:

  1. Primitive type 不像 Object type 一樣本身提供了 method 可以使用,它只提供了相對應的 wrapper objects 如: StringNumberBoolean ... 等。
  2. Primitives 為 Immutable 而 Object 為 Mutable 。

第 1. 因為不是本篇的重點就先容許筆者略過。

那麼第 2. 又是什麼意思呢?讓我們先用一個簡單的 Object 來介紹

var personA = { name: 'Alan', age: 40, title: 'RD' };
var personB = personA;
personB.title = 'Team Lead';
console.log(personA.title) // 'Team Lead'
console.log(personB.title) // 'Team Lead'

明明就是更改 personB 的 title property 怎麼連 personA 的值都被我改掉了呢?

這是因為我們在第二行 code 做的事情是將 personB 的位址指向 personA 的位址,而不是為 personB 重新建立一個新的 value 並讓它指向這個 value ,兩者都指向了同樣的位址的情形下更動任何一方的意思都一樣是更動那個『他們一同指向的位址的值』。

而這也正是 Mutable 所代表的涵意,其實非常直觀,就是 Object type 的 value 是可以被更改的,而被定義為 Immutable 的 Primitive type 的 value 不能被更改。

可是當我指派一個原始型別的值給一個變數,例如:var ex = 'example string' 時,我是可以更改他的值的,這與原始型別不可修正這項特性並不符合吧?這又要怎麼解釋呢?


這其中的重點在於,我們正在討論的對象究竟是『變數( variable )』還是『值( value )』。

我們沒辦法更改 1 或是 'string' 這些值本身,它們就存在於記憶體上的某個位置,但是變數就不同了。

執行 var ex = 'example string' 時,我們做的事情是建立一個新的『變數』並將它指向 'example string' 這組 primitive value 存在的位置。當我們去更改這組變數的值,例如執行 ex = 'example string 2' 時,我們其實是更改 ex 這個變數在記憶體中指向的位置而已,並非更改 Primitive value 本身。

我們在 Primitive 的 MDN 介紹上也能看到它特意區分出 value 與 variable 之間的差異:

『 It is important not to confuse a primitive itself with a variable assigned a primitive value. The variable may be reassigned a new value, but the existing value can not be changed in the ways that objects, arrays, and functions can be altered. 』

關於 Primitive 與 Object types 彼此之間的比較,讀者有興趣可以參考 這篇文章 ,裡面介紹的很詳細。

Why


到目前為止我算理解了 Immutable 與 Mutable 之間的差別了,其實說穿了就只是能不能改變的差別而已嘛。但又為什麼要刻意避開 Mutable 的特性呢? Primitive types 就算了,它是本身就被定義為 Immutable ,為什麼連 Object types 都要拐一個彎讓它變為 Immutable 的呢?


其實 Mutable 還是有它的好處在的,像是能節省記憶體等等。但凡事必有前因後果,會這麼積極地取代掉它是因為它有幾個致命的缺陷:

  1. 為了程式碼可讀性著想:這種隨便你改的特性明擺著就是會產生許多 Side Effect ,隨著 code base 愈加龐大,上面提到的那種『改 A 壞 B 』的狀況也會變得難以一眼就看出問題所在。

    尤其像 slate 這種以 DOM like 為特色的套件, Document 裡時常會有許多 Nodes 同時存在,又會很頻繁的去異動裡頭的資料,讓整組資料皆為 Mutable 的話可想而知會非常難管理,三不五時就因為 reference 而出問題也是可以預見的。

    外加上 Functional Programming 在 JS 上的興起,為了能製作出 Pure function , Immutability 這項特性就顯得格外重要。

    這邊因為擔心篇幅問題就不另外安插 Functional Programming 與 Pure function 的探討了。
  2. 為了觸發 Component 的 state update :有寫 React 的朋友們想必都會經歷過——明明資料更新了卻沒有觸發畫面跟著更新的問題。當然造成這種狀況的原因有很多種,但其中很常見的原因就是 Primitive 與 Object type 分別為傳值與傳址的特性。

    因為 Object type variable 前後指向的位址相同,也因此沒有成功觸發 re-render 機制,結果就是乾脆每次更新都給一個指向全新位址的 variable 強制觸發畫面更新。

通常在實務上最主要是為了迴避 1. ,也就是拿空間來提升程式碼的可讀性以及可維護性。

那麼理解了它存在的原因後我們接著當然就要來談談該如何實作它囉!

How


關於如何在平時寫 JS 時實踐 Immutable 的特性,我們在 JS 本身提供的 Object methods 裡已經可以找到初步的答案。

這些 methods 也都個別被分成了 Mutable 與 Immutable ,諸如 Object.assign()Array.map()Array.slice() ... 這類的 methods 在呼叫時都不會更改到原本的 Object value ,是為 immutable 的,其中 Object.assign() 更是在 es6 出現,拿來複製 JSON Object 的 method 。

let obj = { name: 'Ian' };
let copy = Object.assign({}, obj);

// 更改 copy.name 的值
copy.name = 'John';

// 輸出
console.log(obj.name);  // Ian
console.log(copy.name); // John

或是我們也可以使用 Spread syntax (擴展語法)來達成 copy 這件事:

let obj = { name: 'Ian' }
let copy = { ...obj };

copy.name = 'Alan';

console.log(obj.name); // Ian
console.log(copy.name); // Alan

這些都是我們在處理 Immutable copy 時會使用到的方法。

BUT !人生往往就是那個 BUT !

這些方法僅能處理淺層的複製( shallow copy ),拿 Object.assign() 來舉例:

let obj = { name: 'Ian', car: { type: 'Toyota' } };
let copy = Object.assign({}, obj);

copy.name = 'John';
copy.car.type = 'BMW';

// Output
console.log(obj);  // { name: 'Ian', car: { type: 'BMW' } }
console.log(copy); // { name: 'John', car: { type: 'BMW' } }

可以發現第一層我們確實是成功複製了,但第二層的 type 卻沒有,你可能會說 Spread syntax 可以解決這個問題吧?

確實你沒有說錯,但如果層數很多就會長的像下方這樣

let obj = { a: { b: { c: { d: e: 'deeeeeeeeeeep' } } } };

let copy = {
	...obj,
	a: {
		...obj.a,
		b: {
			...obj.a.b,
			c: {
				...obj.a.b.c,
				d: {
					...obj.a.b.c.d,
				},
			},
		},
	},
}

是不是光看著都覺得不舒服呢...

於是 Deep copy 這項議題就出現了,我們可以透過 lodash 的 cloneDeep() 來協助我們對資料進行 Deep copy 。

或者,我們也能使用一些知名的 Library 如: Immer.js 、 Facebook 團隊製作的 Immutable.js 。協助我們直接打造一個 Immutable 的資料結構,而 Immer 正是 slate 所做的選擇。

最後來提供同樣也是網路上知名的 WYSIWYG 套件: ProseMirror 。所提供的 Immutable library 列表


結束了對 Immutable 這項議題的探討後,下一篇我們會先稍微介紹一下 Immutable.js 與 Immer.js 之間的差異,原因是因為舊版的 slate 其實也是使用 Immutable.js 作為 Immutable data model 的實作工具,是在新版以後才轉移到 Immer.js ,我們甚至能在 Slate 的 Github 上找到這項議題的討論串。

比較並介紹完一些歷史小淵源後我們要接著來學習 Immer.js 的使用概念,以及 slate 的內部又是如何使用 Immer.js 的。

讓我們一樣下一篇見吧!


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

尚未有邦友留言

立即登入留言