React 中的不可变数据 — Immer
Immer 是什么?
Immer 是一個(gè)不可變數(shù)據(jù)的 Javascript 庫(kù),讓你更方便的處理不可變數(shù)據(jù)。
什么是不可變數(shù)據(jù)?
不可變數(shù)據(jù)概念來(lái)源于函數(shù)式編程。函數(shù)式編程中,對(duì)已初始化的“變量”是不可以更改的,每次更改都要?jiǎng)?chuàng)建一個(gè)新的“變量”。
Javascript 在語(yǔ)言層沒(méi)有實(shí)現(xiàn)不可變數(shù)據(jù),需要借助第三方庫(kù)來(lái)實(shí)現(xiàn)。Immer 就是其中一種實(shí)現(xiàn)(類(lèi)似的還有 immutable.js)。
為什么使用不可變數(shù)據(jù)?
在 React 性能優(yōu)化一節(jié)中用了很長(zhǎng)篇幅來(lái)介紹shouldComponentUpdate,不可變數(shù)據(jù)也是由此引出。使用不可變數(shù)據(jù)可以解決性能優(yōu)化引入的問(wèn)題,所以重點(diǎn)介紹這一部分背景。
React 中的性能優(yōu)化
避免調(diào)停(Avoid Reconciliation)
當(dāng)一個(gè)組件的props或state變更,React 會(huì)將最新返回的元素與之前渲染的元素進(jìn)行對(duì)比,以此決定是否有必要更新真實(shí)的 DOM。當(dāng)它們不相同時(shí),React 會(huì)更新該 DOM。雖然 React 已經(jīng)保證未變更的元素不會(huì)進(jìn)行更新,但即使 React 只更新改變了的 DOM 節(jié)點(diǎn),重新渲染仍然花費(fèi)了一些時(shí)間。在大部分情況下它并不是問(wèn)題,不過(guò)如果它已經(jīng)慢到讓人注意了,你可以通過(guò)覆蓋生命周期方法shouldComponentUpdate來(lái)進(jìn)行提速。該方法會(huì)在重新渲染前被觸發(fā)。其默認(rèn)實(shí)現(xiàn)總是返回true,讓 React 執(zhí)行更新:
shouldComponentUpdate(nextProps, nextState) {
return true;
}
如果你知道在什么情況下你的組件不需要更新,你可以在shouldComponentUpdate中返回false來(lái)跳過(guò)整個(gè)渲染過(guò)程。其包括該組件的render調(diào)用以及之后的操作。
shouldComponentUpdate 的作用
這是一個(gè)組件的子樹(shù)。每個(gè)節(jié)點(diǎn)中,SCU代表shouldComponentUpdate返回的值,而vDOMEq代表返回的 React 元素是否相同。最后,圓圈的顏色代表了該組件是否需要被調(diào)停(Reconciliation)。
節(jié)點(diǎn) C2 的shouldComponentUpdate返回了false,React 因而不會(huì)調(diào)用 C2 的render,也因此 C4 和 C5 的shouldComponentUpdate不會(huì)被調(diào)用到。
對(duì)于 C1 和 C3,shouldComponentUpdate返回了true,所以 React 需要繼續(xù)向下查詢(xún)子節(jié)點(diǎn)。這里 C6 的shouldComponentUpdate返回了true,同時(shí)由于render返回的元素與之前不同使得 React 更新了該 DOM。
最后一個(gè)有趣的例子是 C8。React 需要調(diào)用這個(gè)組件的render,但是由于其返回的 React 元素和之前相同,所以不需要更新 DOM。
顯而易見(jiàn),你看到 React 只改變了 C6 的 DOM。對(duì)于 C8,通過(guò)對(duì)比了渲染的 React 元素跳過(guò)了真實(shí) DOM 的渲染。而對(duì)于 C2 的子節(jié)點(diǎn)和 C7,由于shouldComponentUpdate使得render并沒(méi)有被調(diào)用。因此它們也不需要對(duì)比元素了。
示例
上一小節(jié)有一個(gè)有趣的例子 C8,它完全沒(méi)有發(fā)生改變,React 卻還是對(duì)它進(jìn)行了調(diào)停(Reconciliation)。我們完全可以通過(guò)條件判斷來(lái)避免此類(lèi)問(wèn)題,避免調(diào)停(Reconciliation),優(yōu)化性能。
如果你的組件只有當(dāng)props.color或者state.count的值改變才需要更新時(shí),你可以使用shouldComponentUpdate來(lái)進(jìn)行檢查:
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是否改變。如果這些值沒(méi)有改變,那么這個(gè)組件不會(huì)更新。如果你的組件更復(fù)雜一些,你可以使用類(lèi)似“淺比較”的模式來(lái)檢查props和state中所有的字段,以此來(lái)決定是否組件需要更新。React 已經(jīng)提供了一位好幫手來(lái)幫你實(shí)現(xiàn)這種常見(jiàn)的模式 - 你只要繼承React.PureComponent就行了(函數(shù)組件使用React.memo)。所以這段代碼可以改成以下這種更簡(jiǎn)潔的形式:
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只進(jìn)行淺比較,所以當(dāng)props或者state某種程度是可變的話,淺比較會(huì)有遺漏,那你就不能使用它了。比如使用了數(shù)組或?qū)ο螅海ㄒ韵麓a是錯(cuò)誤的)
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數(shù)組使用push方法添加了一個(gè)元素,但state持有的words的引用并沒(méi)有發(fā)生變化。push直接改變了數(shù)據(jù)本身,并沒(méi)有產(chǎn)生新的數(shù)據(jù),淺比較無(wú)法感知到這種變化。React 會(huì)產(chǎn)生錯(cuò)誤的行為,不會(huì)重新執(zhí)行render。為了性能優(yōu)化,引入了另一個(gè)問(wèn)題。
不可變數(shù)據(jù)的力量
避免該問(wèn)題最簡(jiǎn)單的方式是避免更改你正用于props或state的值。例如,上面handleClick方法可以用concat重寫(xiě):
handleClick() {
this.setState(state => ({
words: state.words.concat(['marklar'])
}));
}
或者使用 ES6 數(shù)組擴(kuò)展運(yùn)算符:
handleClick() {
this.setState(state => ({
words: [...state.words, 'marklar'],
}));
};
但是當(dāng)處理深層嵌套對(duì)象時(shí),以 immutable(不可變)的方式更新它們令人費(fèi)解。比如可能寫(xiě)出這樣的代碼:
handleClick() {
this.setState(state => ({
objA: {
...state.objA,
objB: {
...state.objA.objB,
objC: {
...state.objA.objB.objC,
stringA: 'string',
}
},
},
}));
};
我們需要一個(gè)更友好的庫(kù)幫助我們直觀的使用 immutable(不可變)數(shù)據(jù)。
為什么不使用深拷貝/比較?
深拷貝會(huì)讓所有組件都接收到新的數(shù)據(jù),讓shouldComponentUpdate失效。深比較每次都比較所有值,當(dāng)數(shù)據(jù)層次很深且只有一個(gè)值變化時(shí),這些比較是對(duì)性能的浪費(fèi)。
視圖層的代碼,我們希望它更快響應(yīng),所以使用 immutable 庫(kù)進(jìn)行不可變數(shù)據(jù)的操作,也算是一種空間換時(shí)間的取舍。
為什么是 Immer?
immutable.js
自己維護(hù)了一套數(shù)據(jù)結(jié)構(gòu),Javascript 的數(shù)據(jù)類(lèi)型和immutable.js的類(lèi)型需要相互轉(zhuǎn)換,對(duì)數(shù)據(jù)有侵入性。
庫(kù)的體積比較大(63KB),不太適合包體積緊張的移動(dòng)端。
API 極其豐富,學(xué)習(xí)成本較高。
兼容性非常好,支持 IE 較老的版本。
immer
使用 Proxy 實(shí)現(xiàn),兼容性差。
體積很小(12KB),移動(dòng)端友好。
API 簡(jiǎn)潔,使用 Javascript 自己的數(shù)據(jù)類(lèi)型,幾乎沒(méi)有理解成本。
優(yōu)缺點(diǎn)對(duì)比之下,immer 的兼容性缺點(diǎn)在我們的環(huán)境下完全可以忽略。使用一個(gè)不帶來(lái)其他概念負(fù)擔(dān)的庫(kù)還是要輕松很多的。
Immer 概覽
Immer 基于copy-on-write機(jī)制。
Immer 的基本思想是,所有更改都應(yīng)用于臨時(shí)的draftState,它是currentState的代理。一旦完成所有變更,Immer 將基于草稿狀態(tài)的變更生成nextState。這意味著可以通過(guò)簡(jiǎn)單地修改數(shù)據(jù)而與數(shù)據(jù)進(jìn)行交互,同時(shí)保留不可變數(shù)據(jù)的所有優(yōu)點(diǎn)。
本節(jié)圍繞produce這個(gè)核心 API 做介紹。Immer 還提供了一些輔助性 API,詳見(jiàn)官方文檔。
核心 API:produce
語(yǔ)法1:
produce(currentState, recipe: (draftState) => void | draftState, ?PatchListener): nextState
語(yǔ)法2:
produce(recipe: (draftState) => void | draftState, ?PatchListener)(currentState): nextState
使用 produce
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
})
上面的示例中,對(duì)draftState的修改都會(huì)反映到nextState上,并且不會(huì)修改baseState。而 immer 使用的結(jié)構(gòu)是共享的,nextState在結(jié)構(gòu)上與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第一個(gè)參數(shù)傳遞函數(shù)時(shí)將會(huì)進(jìn)行柯理化。它會(huì)返回一個(gè)函數(shù),該函數(shù)接收的參數(shù)會(huì)被傳遞給produce柯理化時(shí)接收的函數(shù)。
示例:
// 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}])
可以很好的利用這種機(jī)制簡(jiǎn)化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 的返回值
通常,recipe不需要顯示的返回任何東西,draftState會(huì)自動(dòng)作為返回值反映到nextState。你也可以返回任意數(shù)據(jù)作為nextState,前提是draftState沒(méi)有被修改。
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
}
})
很顯然,這樣的方式無(wú)法返回undefined。
produce({}, draft => {
// don't do anything
})
produce({}, draft => {
// Try to return undefined from the producer
return undefined
})
因?yàn)樵?Javascript 中,不返回任何值和返回undefined是一樣的,函數(shù)的返回值都是undefined。如果你希望 immer 知道你確實(shí)想要返回undefined怎么辦?
使用 immer 內(nèi)置的變量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'
Auto freezing(自動(dòng)凍結(jié))
Immer 會(huì)自動(dòng)凍結(jié)使用produce修改過(guò)的狀態(tài)樹(shù),這樣可以防止在變更函數(shù)外部修改狀態(tài)樹(shù)。這個(gè)特性會(huì)帶來(lái)性能影響,所以需要在生產(chǎn)環(huán)境中關(guān)閉。可以使用setAutoFreeze(true / false)打開(kāi)或者關(guān)閉。在開(kāi)發(fā)環(huán)境中建議打開(kāi),可以避免不可預(yù)測(cè)的狀態(tài)樹(shù)更改。
在 setState 中使用 immer
使用 immer 進(jìn)行深層狀態(tài)更新很簡(jiǎn)單:
/**
* 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內(nèi)部做你想要做的狀態(tài)變更。符合直覺(jué),不引入新概念。
以 hook 方式使用 immer
Immer 同時(shí)提供了一個(gè) React hook 庫(kù)use-immer用于以 hook 方式使用 immer。
useImmer
useImmer和useState非常像。它接收一個(gè)初始狀態(tài),返回一個(gè)數(shù)組。數(shù)組第一個(gè)值為當(dāng)前狀態(tài),第二個(gè)值為狀態(tài)更新函數(shù)。狀態(tài)更新函數(shù)和produce中的recipe一樣運(yùn)作。
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>
);
}
很顯然,對(duì)這個(gè)例子來(lái)講,無(wú)法體現(xiàn) immer 的作用:)。只是個(gè)展示用法的例子。
useImmerReducer
對(duì)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>
</>
);
}
漫思
總結(jié)
以上是生活随笔為你收集整理的React 中的不可变数据 — Immer的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Geolocation :基于浏览器的定
- 下一篇: 小班教案《小鸡快跑》反思