目录
- 遇到的问题
- Immer 基本使用
- 高阶 produce
- use-immer
- 总结
遇到的问题
React 项目中,无论是在组件中还是在 Redux 中我们去改变 state,都必须返回一个副本 state,而不能在原有的 state 中直接更改。
当数据量非常小的时候没有太大的问题,但是当我们遇到一个 state 是超大的对象或者数组问题就显现出来了,比如我们去更新 lat 字段:
const [state, setState] = useState({
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "[email protected]",
"address": {
"street": "Kulas Light",
"suite": "Apt. 556",
"city": "Gwenborough",
"zipcode": "92998-3874",
"geo": {
"lat": "-37.3159",
"lng": "81.1496"
}
},
"phone": "1-770-736-8031 x56442",
"website": "hildegard.org",
"company": {
"name": "Romaguera-Crona",
"catchPhrase": "Multi-layered client-server neural-net",
"bs": "harness real-time e-markets"
}
});
setState 的写法就会是这样:
setState({
...state,
address: {
...state.address,
geo: {
...state.address.geo,
lat: "88.8888"
}
}
})
这种写法就非常的糟心了,我只需要更新一个字段,却需要三次强制解构,这种情况,只要一不小心就会出现问题,真心讲,我项目中有一个确实因为这个问题出现过 bug 。
Redux 为什么需要返回一个新的 state?
因为在 Redux 中,状态被视为不可变的,永远不应该直接修改(这是因为 UI 需要更新会对比简单暴力对比 state 的状态,所以无论是对象还是数组你直接修改数据,引用地址是不变的,必须返回一个新的),而是通过复制现有的对象/数组,然后修改副本。
好了,我们找到了痛点,怎么去解决呢,接下里就有请今日主角出场。
现在最好的一个解决方案就是使用——Immer 采用数据的双向绑定,更改数据让原数据自动改变。
Immer 的核心实现就是 Vue 的双向绑定原理,优先会使用 proxy,如果不支持会降级到 Object.definedProperty 来实现。
不过,话说回来,在 React 中去支持双向绑定,难道直接用 Vue 不香吗。
Immer 基本使用
我们知道 React 周边的技术栈,都很难学,主要是概念一大推,不过不用担心 Immer 难学,因为它的核心 API 只有一个 就是 produce。
首先项目中安装 Immer:
npm install -D immer
接下来,我们看看如何把上面那个糟心的案例给修改成功:
import { produce } from "immer";
const state = {
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "[email protected]",
"address": {
"street": "Kulas Light",
"suite": "Apt. 556",
"city": "Gwenborough",
"zipcode": "92998-3874",
"geo": {
"lat": "-37.3159",
"lng": "81.1496"
}
},
"phone": "1-770-736-8031 x56442",
"website": "hildegard.org",
"company": {
"name": "Romaguera-Crona",
"catchPhrase": "Multi-layered client-server neural-net",
"bs": "harness real-time e-markets"
}
};
const nextState = produce(state, (draftState) => {
draftState.address.geo.lat = "88.8888";
});
setState(nextState);
这就很简单了,我们通过 produce 函数传入需要更改的 state,然后在回调参数里面获取被双向绑定的 state,更改完成之后,返回已经被更改的 state。
过程就是辣么简单,但是到 Immer 这里描述可就不是那么的简单了,双向绑定之后的 state Immer 给出了一个新概念 draft,下面是一张显示 Immer 工作原理的图片(取自官方文档):
Immer 提供了一个辅助函数,它将一个状态作为参数并生成一个可以直接修改的草稿状态,然后根据所有应用的更改创建一个新的状态对象。
高阶 produce
上面的 produce 案例,有点太 low,高级点我们可以稍微进行封装下,传入要修改的 key 和要修改的 value。
提起代码如下:
const changeGeoLat = (key, value) => {
return produce(state, (draftState) => {
draftState.address.geo[key] = value;
})
};
setState(changeGeoLat("lat", "88.8888"));
这种情况下,produce 提供了一种更加便利的操作:
const changeGeoLat = produce((draftState, key, value) => {
draftState.address.geo[key] = value;
});
console.log(changeGeoLat(state, "lat", "88.8888"));
省略了一层函数,其实并没有省略,我们从源码层面看下文件路径:node_modules/immer/src/core/immerClass.ts
export class Immer implements ProducersFns {
produce: IProduce = (base: any, recipe?: any, patchListener?: any) => {
// curried invocation
if (typeof base === "function" && typeof recipe !== "function") {
const defaultBase = recipe
recipe = base
const self = this
return function curriedProduce(
this: any,
base = defaultBase,
...args: any[]
) {
return self.produce(base, (draft: Drafted) => recipe.call(this, draft, ...args)) // prettier-ignore
}
}
}
}
Immer 会判断你传入的第一个参数 base 是不是函数,如果是,函数 produce 定义了一个高阶函数,会把通过 call 自动调用 recipe。
好了,了解到这完全可以了,可能这个 Curried producers 的概念比较难, 但是 Immer 的基础核心 API produce 还是比较简单的。
use-immer
Immer 的思想是好的,但是在 hook 中这样使用过于麻烦毕竟每次改变都需要 去调用 produce。
其实不难想到,最方便使用 Immer 就是下面这样:
const [count, setCount] = produce(10)
setCount(count++);
把 useState 的逻辑封装到 produce 中,通过 produce 返回的状态直接就是双向绑定的。
这解释今天的第二主角 use-immer。
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 (
Hello {person.name} ({person.age})
{
updateName(e.target.value);
}}
value={person.name}
/>
);
}
另外,如果是数字、字符串、布尔值为了方便可以不使用回调操作,就是下面这样:
const [count, setCount] = useImmer(0);
const incrementFatherAge = () => {
setCount(count + 1);
};
use-immer 还提供另外一个 API useImmerReducer,这个留给大家探索吧。
总结
今天我们学习了,在使用 React 维护状态的过程中,当遇到嵌套过深的数组或对象时,每次更改都要小心意义。
于是我们找到了 Immer 这个库,用双向绑定来解决必须返回一个副本,为了能够更好的和 Hook 结合,我们又学习了use-immer。
最后,如果您在使用 React,而还没有使用过它们,我强烈建议您仔细阅读文档并立即开始用起来。
用着用着你就会发现还是他娘的 Vue 好用,哈哈哈哈。
今天国庆了,大家是去旅游了,还是回家收玉米了呢?不过今天我要去看个电影「长津湖」,票价有点贵,但是重工业电影还是大屏看着爽,走起,大家国庆快乐!。