大家在設計網頁的時候,時常會遇到一些時常忽略的問題,例如明明更新了值,網頁的畫面卻沒有改變。如果你遇到了這個問題,那麼你可能忽略了Immutable的重要性了。
認識Immutable
Immutable在OOP的design pattern中,時不時會提出來講,儘管嚴格來說,Immutable不算是設計樣式的一種,但是設計程式的好壞,往往影響程式涉及很廣的層面,以下就來認識吧。
為什麼需要它
Immutable的設計概念,可以讓開發者更容易掌握資料。
有時候程式語言的特性和蜜糖,讓開發者太仰賴其中的滋味,而讓程式碼變得越來越髒。
例如:call by reference可以讓開發者經由傳遞指定的位置,直接修改內容。
但是問題來了,你要怎麼掌握你的資料是舊的還是新的,已經更新過了呢?
迷:資料再拿出來看就好了呀!
- 可以這樣做沒錯,可是你要怎麼確定,就是這筆?時間點?
迷:放個戳記就好了呀!
因此,Immutable就出來了
特點
在記憶體中,變數宣告之後,會賦予一個空間reference給變數
我們無法馬上得知變數是否已經更動,但是我們卻可以馬上知道這個記憶體位置存的是什麼值。
因此我們只需要確保每次更動內容的同時,產生一個新物件,並且複製一樣的內容過去,再來判斷記憶體位置一不一樣,即可立即判斷這個值是否更新過了!
重點整理如下:
- 沒有side effects
- 容易追蹤資料
實際例子
在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,將物件整個展開到原本的位置
這個動作主要是為了達到以下目的:
- 將物件展開之後,即視為全新的儲存位置
- 先展開物件再用新的值覆蓋,即視為更新值
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的重要性了嗎?
記得實際去演練一下,就會發現魔鬼總是藏在細節裡。