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 的。

让我们一样下一篇见吧!


<<:  Day17 Combine 04 - Operators 主要类型

>>:  初学者跪着学JavaScript Day17: 物件:new Set()

[从0到1] C#小乳牛 练成基础程序逻辑 Day 13 - if 条件判断

单一条件判断 | Y/N是非题般的存在 | 流程图绘制 ...

[ Day 24 ] - 阵列资料处理 - filter

特性与用途 不会影响到原始阵列的资料 可以筛选符合条件的内容,并且回传至新的阵列 直接进入写法及范例...

做不好资金控管,一档大赔,获利全吐

最近有位朋友抱怨,跟我的单但绩效却跟我差很多 我第一个直觉反应就是他没做好「资金控管」 因为很重要,...

[DAY 21]纠团通知功能(1/3)

先前做的公会文字云 其中任务、副本、主线的出现次数很多代表频道里蛮常有人想纠团的 但我翻了一下纪录成...

Vue CLI建置 & GitHub上传

昨天我们安装好Vue cli今天要来新增一个专案啦~在官网中它介绍了两种方法,那这里会用第二种方法实...