美文网首页React基础前端
Redux 中更改复杂 state 的好办法—Immer

Redux 中更改复杂 state 的好办法—Immer

作者: CondorHero | 来源:发表于2021-09-30 20:13 被阅读0次
    Redux 中更改复杂 state 的好办法—Immer.png

    目录

    遇到的问题

    React 项目中,无论是在组件中还是在 Redux 中我们去改变 state,都必须返回一个副本 state,而不能在原有的 state 中直接更改。

    当数据量非常小的时候没有太大的问题,但是当我们遇到一个 state 是超大的对象或者数组问题就显现出来了,比如我们去更新 lat 字段:

    const [state, setState] = useState({
        "id": 1,
        "name": "Leanne Graham",
        "username": "Bret",
        "email": "Sincere@april.biz",
        "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": "Sincere@april.biz",
        "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 原理

    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 (
            <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>
        );
    }
    

    另外,如果是数字、字符串、布尔值为了方便可以不使用回调操作,就是下面这样:

    const [count, setCount] = useImmer(0);
    
    const incrementFatherAge = () => {
      setCount(count + 1);
    };
    

    use-immer 还提供另外一个 API useImmerReducer,这个留给大家探索吧。

    总结

    今天我们学习了,在使用 React 维护状态的过程中,当遇到嵌套过深的数组或对象时,每次更改都要小心意义。

    于是我们找到了 Immer 这个库,用双向绑定来解决必须返回一个副本,为了能够更好的和 Hook 结合,我们又学习了use-immer。

    最后,如果您在使用 React,而还没有使用过它们,我强烈建议您仔细阅读文档并立即开始用起来。

    用着用着你就会发现还是他娘的 Vue 好用,哈哈哈哈。

    今天国庆了,大家是去旅游了,还是回家收玉米了呢?不过今天我要去看个电影「长津湖」,票价有点贵,但是重工业电影还是大屏看着爽,走起,大家国庆快乐!。

    相关文章

      网友评论

        本文标题:Redux 中更改复杂 state 的好办法—Immer

        本文链接:https://www.haomeiwen.com/subject/racfnltx.html