Immer 是一個不可變數據的 Javascript 庫,讓你更方便的處理不可變數據。html
不可變數據概念來源於函數式編程。函數式編程中,對已初始化的「變量」是不能夠更改的,每次更改都要建立一個新的「變量」。react
Javascript 在語言層沒有實現不可變數據,須要藉助第三方庫來實現。Immer 就是其中一種實現(相似的還有 immutable.js)。git
在 React 性能優化一節中用了很長篇幅來介紹 shouldComponentUpdate
,不可變數據也是由此引出。使用不可變數據能夠解決性能優化引入的問題,因此重點介紹這一部分背景。github
當一個組件的 props
或 state
變動,React 會將最新返回的元素與以前渲染的元素進行對比,以此決定是否有必要更新真實的 DOM。當它們不相同時,React 會更新該 DOM。雖然 React 已經保證未變動的元素不會進行更新,但即便 React 只更新改變了的 DOM 節點,從新渲染仍然花費了一些時間。在大部分狀況下它並非問題,不過若是它已經慢到讓人注意了,你能夠經過覆蓋生命週期方法 shouldComponentUpdate
來進行提速。該方法會在從新渲染前被觸發。其默認實現老是返回 true
,讓 React 執行更新:typescript
shouldComponentUpdate(nextProps, nextState) {
return true;
}
複製代碼
若是你知道在什麼狀況下你的組件不須要更新,你能夠在 shouldComponentUpdate
中返回 false
來跳過整個渲染過程。其包括該組件的 render
調用以及以後的操做。編程
這是一個組件的子樹。每一個節點中,SCU
表明 shouldComponentUpdate
返回的值,而 vDOMEq
表明返回的 React 元素是否相同。最後,圓圈的顏色表明了該組件是否須要被調停(Reconciliation)。 redux
shouldComponentUpdate
返回了
false
,React 於是不會調用 C2 的
render
,也所以 C4 和 C5 的
shouldComponentUpdate
不會被調用到。
對於 C1 和 C3,shouldComponentUpdate
返回了 true
,因此 React 須要繼續向下查詢子節點。這裏 C6 的 shouldComponentUpdate
返回了 true
,同時因爲 render
返回的元素與以前不一樣使得 React 更新了該 DOM。api
最後一個有趣的例子是 C8。React 須要調用這個組件的 render
,可是因爲其返回的 React 元素和以前相同,因此不須要更新 DOM。數組
顯而易見,你看到 React 只改變了 C6 的 DOM。對於 C8,經過對比了渲染的 React 元素跳過了真實 DOM 的渲染。而對於 C2 的子節點和 C7,因爲 shouldComponentUpdate
使得 render
並無被調用。所以它們也不須要對比元素了。性能優化
上一小節有一個有趣的例子 C8,它徹底沒有發生改變,React 卻仍是對它進行了調停(Reconciliation)。咱們徹底能夠經過條件判斷來避免此類問題,避免調停(Reconciliation),優化性能。
若是你的組件只有當 props.color
或者 state.count
的值改變才須要更新時,你可使用 shouldComponentUpdate
來進行檢查:
class CounterButton extends React.Component {
constructor(props) {
super(props);
this.state = {count: 1};
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props.color !== nextProps.color) {
return true;
}
if (this.state.count !== nextState.count) {
return true;
}
return false;
}
render() {
return (
<button color={this.props.color} onClick={() => this.setState(state => ({count: state.count + 1}))}> Count: {this.state.count} </button>
);
}
}
複製代碼
在這段代碼中,shouldComponentUpdate
僅檢查了 props.color
或 state.count
是否改變。若是這些值沒有改變,那麼這個組件不會更新。若是你的組件更復雜一些,你可使用相似「淺比較」的模式來檢查 props
和 state
中全部的字段,以此來決定是否組件須要更新。React 已經提供了一位好幫手來幫你實現這種常見的模式 - 你只要繼承 React.PureComponent
就好了(函數組件使用 React.memo
)。因此這段代碼能夠改爲如下這種更簡潔的形式:
class CounterButton extends React.PureComponent {
constructor(props) {
super(props);
this.state = {count: 1};
}
render() {
return (
<button color={this.props.color} onClick={() => this.setState(state => ({count: state.count + 1}))}> Count: {this.state.count} </button>
);
}
}
複製代碼
但 React.PureComponent
只進行淺比較,因此當 props
或者 state
某種程度是可變的話,淺比較會有遺漏,那你就不能使用它了。好比使用了數組或對象:(如下代碼是錯誤的)
class ListOfWords extends React.PureComponent {
render() {
return <div>{this.props.words.join(',')}</div>;
}
}
class WordAdder extends React.Component {
constructor(props) {
super(props);
this.state = {
words: ['marklar']
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// 這部分代碼很糟,並且還有 bug
const words = this.state.words;
words.push('marklar');
this.setState({words: words});
}
render() {
return (
<div>
<button onClick={this.handleClick} />
<ListOfWords words={this.state.words} />
</div>
);
}
}
複製代碼
words
數組使用 push
方法添加了一個元素,但 state
持有的 words
的引用並無發生變化。push
直接改變了數據自己,並無產生新的數據,淺比較沒法感知到這種變化。React 會產生錯誤的行爲,不會從新執行 render
。爲了性能優化,引入了另外一個問題。
避免該問題最簡單的方式是避免更改你正用於 props
或 state
的值。例如,上面 handleClick
方法能夠用 concat
重寫:
handleClick() {
this.setState(state => ({
words: state.words.concat(['marklar'])
}));
}
複製代碼
或者使用 ES6 數組擴展運算符:
handleClick() {
this.setState(state => ({
words: [...state.words, 'marklar'],
}));
};
複製代碼
可是當處理深層嵌套對象時,以 immutable(不可變)的方式更新它們使人費解。好比可能寫出這樣的代碼:
handleClick() {
this.setState(state => ({
objA: {
...state.objA,
objB: {
...state.objA.objB,
objC: {
...state.objA.objB.objC,
stringA: 'string',
}
},
},
}));
};
複製代碼
咱們須要一個更友好的庫幫助咱們直觀的使用 immutable(不可變)數據。
深拷貝會讓全部組件都接收到新的數據,讓 shouldComponentUpdate
失效。深比較每次都比較全部值,當數據層次很深且只有一個值變化時,這些比較是對性能的浪費。
視圖層的代碼,咱們但願它更快響應,因此使用 immutable 庫進行不可變數據的操做,也算是一種空間換時間的取捨。
immutable.js
的類型須要相互轉換,對數據有侵入性。優缺點對比之下,immer 的兼容性缺點在咱們的環境下徹底能夠忽略。使用一個不帶來其餘概念負擔的庫仍是要輕鬆不少的。
Immer 基於 copy-on-write 機制。
Immer 的基本思想是,全部更改都應用於臨時的 draftState,它是 currentState 的代理。一旦完成全部變動,Immer 將基於草稿狀態的變動生成 nextState。這意味着能夠經過簡單地修改數據而與數據進行交互,同時保留不可變數據的全部優勢。
本節圍繞 produce
這個核心 API 作介紹。Immer 還提供了一些輔助性 API,詳見官方文檔。
語法1:
produce(currentState, recipe: (draftState) => void | draftState, ?PatchListener): nextState
語法2:
produce(recipe: (draftState) => void | draftState, ?PatchListener)(currentState): nextState
import produce from "immer"
const baseState = [
{
todo: "Learn typescript",
done: true
},
{
todo: "Try immer",
done: false
}
]
const nextState = produce(baseState, draftState => {
draftState.push({todo: "Tweet about it"})
draftState[1].done = true
})
複製代碼
上面的示例中,對 draftState
的修改都會反映到 nextState
上,而且不會修改 baseState
。而 immer 使用的結構是共享的,nextState
在結構上與 currentState
共享未修改的部分。
// the new item is only added to the next state,
// base state is unmodified
expect(baseState.length).toBe(2)
expect(nextState.length).toBe(3)
// same for the changed 'done' prop
expect(baseState[1].done).toBe(false)
expect(nextState[1].done).toBe(true)
// unchanged data is structurally shared
expect(nextState[0]).toBe(baseState[0])
// changed data not (dûh)
expect(nextState[1]).not.toBe(baseState[1])
複製代碼
給 produce
第一個參數傳遞函數時將會進行柯理化。它會返回一個函數,該函數接收的參數會被傳遞給 produce
柯理化時接收的函數。 示例:
// mapper will be of signature (state, index) => state
const mapper = produce((draft, index) => {
draft.index = index
})
// example usage
console.dir([{}, {}, {}].map(mapper))
// [{index: 0}, {index: 1}, {index: 2}])
複製代碼
能夠很好的利用這種機制簡化 reducer
:
import produce from "immer"
const byId = produce((draft, action) => {
switch (action.type) {
case RECEIVE_PRODUCTS:
action.products.forEach(product => {
draft[product.id] = product
})
return
}
})
複製代碼
一般,recipe
不須要顯示的返回任何東西,draftState
會自動做爲返回值反映到 nextState
。你也能夠返回任意數據做爲 nextState
,前提是 draftState
沒有被修改。
const userReducer = produce((draft, action) => {
switch (action.type) {
case "renameUser":
// OK: we modify the current state
draft.users[action.payload.id].name = action.payload.name
return draft // same as just 'return'
case "loadUsers":
// OK: we return an entirely new state
return action.payload
case "adduser-1":
// NOT OK: This doesn't do change the draft nor return a new state!
// It doesn't modify the draft (it just redeclares it)
// In fact, this just doesn't do anything at all
draft = {users: [...draft.users, action.payload]}
return
case "adduser-2":
// NOT OK: modifying draft *and* returning a new state
draft.userCount += 1
return {users: [...draft.users, action.payload]}
case "adduser-3":
// OK: returning a new state. But, unnecessary complex and expensive
return {
userCount: draft.userCount + 1,
users: [...draft.users, action.payload]
}
case "adduser-4":
// OK: the immer way
draft.userCount += 1
draft.users.push(action.payload)
return
}
})
複製代碼
很顯然,這樣的方式沒法返回 undefined
。
produce({}, draft => {
// don't do anything
})
複製代碼
produce({}, draft => {
// Try to return undefined from the producer
return undefined
})
複製代碼
由於在 Javascript 中,不返回任何值和返回 undefined
是同樣的,函數的返回值都是 undefined
。若是你但願 immer 知道你確實想要返回 undefined
怎麼辦? 使用 immer 內置的變量 nothing
:
import produce, {nothing} from "immer"
const state = {
hello: "world"
}
produce(state, draft => {})
produce(state, draft => undefined)
// Both return the original state: { hello: "world"}
produce(state, draft => nothing)
// Produces a new state, 'undefined'
複製代碼
Immer 會自動凍結使用 produce
修改過的狀態樹,這樣能夠防止在變動函數外部修改狀態樹。這個特性會帶來性能影響,因此須要在生產環境中關閉。可使用 setAutoFreeze(true / false)
打開或者關閉。在開發環境中建議打開,能夠避免不可預測的狀態樹更改。
使用 immer 進行深層狀態更新很簡單:
/** * Classic React.setState with a deep merge */
onBirthDayClick1 = () => {
this.setState(prevState => ({
user: {
...prevState.user,
age: prevState.user.age + 1
}
}))
}
/** * ...But, since setState accepts functions, * we can just create a curried producer and further simplify! */
onBirthDayClick2 = () => {
this.setState(
produce(draft => {
draft.user.age += 1
})
)
}
複製代碼
基於 produce
提供了柯理化的特性,直接將 produce
柯理化的返回值傳遞給 this.setState
便可。在 recipe
內部作你想要作的狀態變動。符合直覺,不引入新概念。
Immer 同時提供了一個 React hook 庫 use-immer
用於以 hook 方式使用 immer。
useImmer
和 useState
很是像。它接收一個初始狀態,返回一個數組。數組第一個值爲當前狀態,第二個值爲狀態更新函數。狀態更新函數和 produce
中的 recipe
同樣運做。
import React from "react";
import { useImmer } from "use-immer";
function App() {
const [person, updatePerson] = useImmer({
name: "Michel",
age: 33
});
function updateName(name) {
updatePerson(draft => {
draft.name = name;
});
}
function becomeOlder() {
updatePerson(draft => {
draft.age++;
});
}
return (
<div className="App"> <h1> Hello {person.name} ({person.age}) </h1> <input onChange={e => { updateName(e.target.value); }} value={person.name} /> <br /> <button onClick={becomeOlder}>Older</button> </div> ); } 複製代碼
很顯然,對這個例子來說,沒法體現 immer 的做用:)。只是個展現用法的例子。
對 useReducer
的封裝:
import React from "react";
import { useImmerReducer } from "use-immer";
const initialState = { count: 0 };
function reducer(draft, action) {
switch (action.type) {
case "reset":
return initialState;
case "increment":
return void draft.count++;
case "decrement":
return void draft.count--;
}
}
function Counter() {
const [state, dispatch] = useImmerReducer(reducer, initialState);
return (
<> Count: {state.count} <button onClick={() => dispatch({ type: "reset" })}>Reset</button> <button onClick={() => dispatch({ type: "increment" })}>+</button> <button onClick={() => dispatch({ type: "decrement" })}>-</button> </> ); } 複製代碼