大家在設計網頁的時候,時常會遇到一些時常忽略的問題,例如明明更新了值,網頁的畫面卻沒有改變。如果你遇到了這個問題,那麼你可能忽略了Immutable的重要性了。

認識Immutable

Immutable在OOP的design pattern中,時不時會提出來講,儘管嚴格來說,Immutable不算是設計樣式的一種,但是設計程式的好壞,往往影響程式涉及很廣的層面,以下就來認識吧。

為什麼需要它

Immutable的設計概念,可以讓開發者更容易掌握資料。
有時候程式語言的特性和蜜糖,讓開發者太仰賴其中的滋味,而讓程式碼變得越來越髒。
例如:call by reference可以讓開發者經由傳遞指定的位置,直接修改內容。
但是問題來了,你要怎麼掌握你的資料是舊的還是新的,已經更新過了呢?

迷:資料再拿出來看就好了呀!

  • 可以這樣做沒錯,可是你要怎麼確定,就是這筆?時間點?

迷:放個戳記就好了呀!

  • 每個Object都要放?不是瘋了嗎?

因此,Immutable就出來了

特點

在記憶體中,變數宣告之後,會賦予一個空間reference給變數
我們無法馬上得知變數是否已經更動,但是我們卻可以馬上知道這個記憶體位置存的是什麼值。
因此我們只需要確保每次更動內容的同時,產生一個新物件,並且複製一樣的內容過去,再來判斷記憶體位置一不一樣,即可立即判斷這個值是否更新過了!

重點整理如下:

  1. 沒有side effects
  2. 容易追蹤資料

實際例子

在React中,我們時常會使用State來控管和改變component,藉由改變State讓Component re-render。
通常我們掌管Component的UI呈現,會focus在State change和Props change。
一旦發生State change或Props change,React會自動的比較Virtual-DOM,如果兩者不一樣的話,將會更新最新的Virtual-DOM節點。

而在React 16.8以前的版本,大家還在普遍使用class-based component時,Immutable的觀念更顯得重要。

以下示範一個簡單的範例,顯示咖啡的庫存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class CoffeeShop extends Component {
state = {
stock: {
general: 5,
latte: 10,
}
};
render () {
const { general, latte } = this.state.stock;
return (
<h1>Stock status list</h1>
<ul>
<li>
<h2>general</h2>
<p>stock: {general}</p>

</li>
<li>
<h2>latte</h2>
<p>stock: {latte}</p>
</li>
</ul>
)
}
}

錯誤示範1

以下進行一個錯誤示範,如果我設計一個控制咖啡庫存的function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class CoffeeShop extends Component {
state = {
stock: {
general: 5,
latte: 10,
}
};

onStockChange = (coffeeType, stock) => {
switch(coffeeType) {
case 'general': {
this.state.stock.general = stock;
break;
}
case 'latte': {
this.state.stock.latte = stock;
break;
}
default: {
break;
}
}
}

render () {
const { general, latte } = this.state.stock;
return (
<h1>Stock status list</h1>
<ul>
<li>
<h2>general</h2>
<p>stock: {general}</p>

</li>
<li>
<h2>latte</h2>
<p>stock: {latte}</p>
</li>
</ul>
)
}
}

有學過基礎程式的人應該都知道,理論上,這是可以達到你要的目的->改值
但是事實上上次提到了,React會根據State change來更新Virtual-DOM
對State而言,最外層的Stock的Reference是不變的,因此React並不會更新Virtual-DOM
這種問題是剛入門React的人可能犯的問題。
基本上盡可能地使用React提供的方法,例如:setState

錯誤示範2

以下再次示範錯誤的使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class CoffeeShop extends Component {
state = {
stock: {
general: 5,
latte: 10,
}
};

onStockChange = (coffeeType, stock) => {
const currentStock = this.state.stock;
switch(coffeeType) {
case 'general': {
currentStock.general = stock;
this.setState({
stock: currentStock,
});
break;
}
case 'latte': {
currentStock.latte = stock;
this.setState({
stock: currentStock,
});
break;
}
default: {
break;
}
}
}

render () {
const { general, latte } = this.state.stock;
return (
<h1>Stock status list</h1>
<ul>
<li>
<h2>general</h2>
<p>stock: {general}</p>

</li>
<li>
<h2>latte</h2>
<p>stock: {latte}</p>
</li>
</ul>
)
}
}

你覺得這裡犯了什麼嚴重的錯誤嗎?仔細觀察想一想
沒錯!Reference也是一樣的,只是將相同記憶體位置的值更改,因此一樣不會有改變。
雖然是錯誤示範,不過React的setState有提供deep contrast的功能
即使是你沒有那些係項的程式概念也能輕鬆上手,算是新手友善的蜜糖
但是還是鼓勵開發者盡可能的注意小細節,很多效能問題都是發生在這種小細節上。

Immutable示範1

讓我們來看看該怎麼做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class CoffeeShop extends Component {
state = {
stock: {
general: 5,
latte: 10,
}
};

onStockChange = (coffeeType, stock) => {
const currentStock = this.state.stock;
switch(coffeeType) {
case 'general': {
this.setState({
...this.state,
stock: {
...currentStock,
general: stock,
},
});
break;
}
case 'latte': {
currentStock.latte = stock;
this.setState({
...this.state,
stock: {
...currentStock,
latte: stock,
},
});
break;
}
default: {
break;
}
}
}

render () {
const { general, latte } = this.state.stock;
return (
<h1>Stock status list</h1>
<ul>
<li>
<h2>general</h2>
<p>stock: {general}</p>

</li>
<li>
<h2>latte</h2>
<p>stock: {latte}</p>
</li>
</ul>
)
}
}

這裡使用spread operator,將物件整個展開到原本的位置
這個動作主要是為了達到以下目的:

  1. 將物件展開之後,即視為全新的儲存位置
  2. 先展開物件再用新的值覆蓋,即視為更新值

Immutable示範2

此外,你也可以使用Object.assign的方式複製出一個新的物件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
onStockChange = (coffeeType, stock) => {
const currentState = Object.assign({}, this.state);
switch(coffeeType) {
case 'general': {
currentState.stock.general = stock;
break;
}
case 'latte': {
currentState.stock.latte = stock;
break;
}
default: {
break;
}
}
this.setState({ ...currentState });
}

PureComponent介紹

以前我在開發class-based component的時候,我習慣盡量用PureComponent來開發我的元件。
除了可以增加渲染效能之外,也可以時常提醒自己每次更新物件都要盡可能的做淺層比較。
因此,你僅僅需要改成這樣即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class CoffeeShop extends PureComponent {
state = {
stock: {
general: 5,
latte: 10,
}
};

onStockChange = (coffeeType, stock) => {
const currentStock = this.state.stock;
switch(coffeeType) {
case 'general': {
this.setState({
...this.state,
stock: {
...currentStock,
general: stock,
},
});
break;
}
case 'latte': {
currentStock.latte = stock;
this.setState({
...this.state,
stock: {
...currentStock,
latte: stock,
},
});
break;
}
default: {
break;
}
}
}

render () {
const { general, latte } = this.state.stock;
return (
<h1>Stock status list</h1>
<ul>
<li>
<h2>general</h2>
<p>stock: {general}</p>

</li>
<li>
<h2>latte</h2>
<p>stock: {latte}</p>
</li>
</ul>
)
}
}

16.8版以後的React

儘管React開始著重在functional programming的使用,Immutable的觀念還是不能忘記。
學好對未來絕對很受用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const CoffeeShop() {
const [stock, setStock] = useState({
general: 5,
latte: 10,
});
const onStockChange = (coffeeType, stock) => {
switch(coffeeType) {
case 'general': {
setStock({
...stock,
general: stock,
});
break;
}
case 'latte': {
currentStock.latte = stock;
this.setState({
...stock,
latte: stock,
});
break;
}
default: {
break;
}
}
}
return (
<h1>Stock status list</h1>
<ul>
<li>
<h2>general</h2>
<p>stock: {general}</p>

</li>
<li>
<h2>latte</h2>
<p>stock: {latte}</p>
</li>
</ul>
);
}

小節

以上示範了錯誤的方式與可以使用的方式,你有更了解Immutable的重要性了嗎?
記得實際去演練一下,就會發現魔鬼總是藏在細節裡。