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> </> ); } 复制代码