React Immer 和 状态管理

React Immer

不可变数据

在 React 中数据发生了变更组件就会重新渲染,为了更高效的判断数据是否变更,React 使用了不可变数据(浅比较),这就导致如果直接修改源数据,组件并不会重新渲染。 更新数据都是需要创建一个新的数据副本,下面我们来演示一下不可变数据.

  1. 我们定义了一个 animal 相关的数据,在 useEffect 修改了 animal 的 name 属性。
javascript 复制代码
export default function Page() {
  const [animal, setAnimal] = useState({
    name: 'cat',
    body: {
      color: 'white',
    },
  });

  useEffect(() => {
    animal.name = 'dog';
    setAnimal(animal);
  }, []);

  console.log(JSON.stringify(animal, null, 2));

  return null;
}

通过最终输出的结果可以看出来,数据并没有发生变更。是因为 React 底层采用的是不可变数据,直接修改数据,虽然影响了 animal 对象属性的值,但 animal 数据本身的内存地址并未发生变化,所以 setAnimal 不会触发数据更新。

  1. 为了数据能更新成功,我们需要创建一个新的对象并且把原有对象复制过来,再复写我们想更新的属性。
scss 复制代码
  useEffect(() => {
    setAnimal({
      ...animal,
      name: 'dog',
    });
  }, []);

输出显示这样可以更新成功。

  1. 假设我们想更新color 值,由于color属性位于嵌套对象的第二层。
ini 复制代码
  useEffect(() => {
    animal.body.color = 'black';
    setAnimal({
      ...animal,
      name: 'dog',
    });
  }, []);

通过操作看到数据确实更新了,在这个场景下看似正确,但是其实我们已经在不知不觉中埋下了一个错误。

  1. 这时候我们把案例变得复杂。我们增加一个 useEffect 希望能监听到 body 属性的变更。
scss 复制代码
  useEffect(()=>{
    console.log("animal.body change", animal.body)
  }, [animal.body])

  useEffect(() => {
    animal.body.color = 'black';
    setAnimal({
      ...animal,
      name: 'dog',
    });
  }, []);

由于代码直接修改了 body 的 color 属性,对于 body 这个对象来说他的内存地址是没有任何变化的,只是内部属性的值发生了变动。所以 useEffect 中的 console 只输出了 color: white color值被二次调整成 black 并未被监听到。

  1. 为了让 body 内部属性的变动可以被监听到,所以我们要再次修改 setAnimal 的逻辑,对于一个有嵌套层级的对象,如果我们要更新内层的某一个属性,一定要将对象每一层都要重新创建一遍(深拷贝)。
scss 复制代码
  useEffect(()=>{
    console.log("animal.body change", animal.body)
  }, [animal.body])

  useEffect(() => {
    setAnimal({
      ...animal,
      name: 'dog',
      body: {
        ...animal.body,
        color: 'black',
      }
    });
  }, []);

这样能准确的监听到 body 属性的变更。 这种方案有两个明显的缺点,1. 如果对象嵌套层数较多,需要按层解构,赋值,代码中会充斥这很多这样的模板代码。2. 为了更新某一个属性却要把所有的值都复制一次,会带来很多不必要的内存浪费 和 更频繁的 GC对应的CPU资源的浪费。

小程序完整代码

javascript 复制代码
export default function Page() {
  const [animal, setAnimal] = useState({
    name: 'cat',
    body: {
      color: 'white',
    },
  });
  
  useEffect(()=>{
    console.log("animal.body change", animal.body)
  }, [animal.body])

  useEffect(() => {
    setAnimal({
      ...animal,
      name: 'dog',
      body: {
        ...animal.body,
        color: 'black',
      }
    });
  }, []);

  console.log(JSON.stringify(animal, null, 2));

  return null;
}

为了解决不可变数据的更新问题,引入了 Immer 这一套解决方案 .

使用 Immer

  • Immer 文档 immerjs.github.io/immer/zh-CN...

    immer 的基本思想是 通过对我们当前的数据 currentState 进行代理 生成 中间态 draftState,更新 drafState 中的数据,immer 会生成新的 nextState。

typescript 复制代码
export default function Home() {
  const [currentData, setCurrentData] = useState<any>({ current: 1 });
  
  useEffect(() => {
    const nextData = produce(currentData, (draftData: any) => {
      draftData.current = 2;
    })
    setCurrentData(nextData);
  }, []);

  console.log('currentData.current', currentData.current);
  return null;
}

// 输出
// currentData.current 1
// currentData.current 2
  • hooks 写法
typescript 复制代码
export default function Home() {
  const [currentData, setCurrentData] = useImmer<any>({ current: 1 });
  
  useEffect(() => {
    setCurrentData((draftState: any) => {
      draftState.current = 2;
    });
  }, []);

  console.log("currentData.current", currentData.current);
  return null;
}

接着我们使用 Immer 对之前的小程序进行改造

javascript 复制代码
export default function Page() {
  const [animal, setAnimal] = useImmer({
    name: 'cat',
    body: {
      color: 'white',
    },
  });

  useEffect(()=>{
    console.log("animal.body change", animal.body)
  }, [animal.body])

  useEffect(() => {
    setAnimal((draft)=> {
      draft.body.color = 'black'
    });
  }, []);

  console.log(JSON.stringify(animal, null, 2));

  return null;
}

同样的功能使用 immer 之后少了很多模板代码,程序变得整洁,而且也不容易出错。

Immer 的特点

  • 写时复制(Copy on Write), copy-on-write 是数据复制的一种方案和设计思想在linux 内核中也有使用 man7.org/linux/man-p...
typescript 复制代码
export default function Page() {
  const [data, setData] = useState({
    d1: { a: 1 },
    d2: { a: 2 },
    d3: { a: 3 },
  });

  const nextState = produce(data, (draftState: any) => {
    console.log(draftState.d1)
  });
  console.log("nextState === data", nextState === data);
  const nextState2 = produce(data, (draftState: any) => {
    draftState.d1.a = 4;
  });
  console.log("nextState2 === data", nextState2 === data);
  console.log("nextState2.d1 === data.d1", nextState2.d1 === data.d1);
  console.log("nextState2.d2 === data.d2", nextState2.d2 === data.d2);

  return null;
}

immer 利用 Proxy 代理原对象,我们不对任何属性做修改时,nextState 和 data 的值相等,说明数据没有发生复制,当 draftState 发生了部分写入的时候,只有原对象的地址和发生修改的地方产生了数据复制,没有写入的部分数据并不会产生数据复制,相对于深拷贝更新来说能减少内存的占用,减少不必要的GC。

buildData 函数

ini 复制代码
const buildData = () => {
  let data: any = {};
  for (let i = 0; i < 1024 * 1024; i++) {
    data[`${i}`] = `${i}`;
  }
  return {
    data,
    data2: 0
  };
};
  1. 深拷贝更新数据时候内存图
typescript 复制代码
export default function Page() {
  const [state, setState] = useState<any>(buildData());
  useEffect(() => {
    const timer = setInterval(() => {
      setState((ctr: any) => {
        return {
          ...ctr,
          data: {
            ...ctr.data,
          },
          data2: 1
        };
      });
    }, 1000);
    return () => clearInterval(timer);
  }, []);
}
  1. 使用 immer 之后的内存图
scss 复制代码
export default function Page() {
  const [state, setState] = useImmer<any>(buildData());
  useEffect(() => {
    const timer = setInterval(() => {
      setState((ctr: any) => {
        ctr.data2 = 1;
      })
    }, 1000);
    return () => clearInterval(timer);
  }, []);
}

虽然上面的例子看上去有些刻意,我的想法是能从逻辑上证明使用immer在某些场景下更新数据是能减少开发者的心智负担,减少程序出现的问题,有时还能减少程序内存的占用和减少CPU的消耗,至少使用immer 之后不会对程序造成太大的负面影响。

在学习的过程中,还发现了一个社区里面一次有趣的讨论 Redux Toolkit 团队不断有用户提 pr ,想要将 immer 变成可选的属性,开发着则认为 immer 对性能影响不大,而且帮助很大应该保留。

相关推荐
gnip1 小时前
企业级配置式表单组件封装
前端·javascript·vue.js
一只叫煤球的猫2 小时前
写代码很6,面试秒变菜鸟?不卖课,面试官视角走心探讨
前端·后端·面试
excel3 小时前
Three.js 材质(Material)详解 —— 区别、原理、场景与示例
前端
掘金安东尼3 小时前
抛弃自定义模态框:原生Dialog的实力
前端·javascript·github
hj5914_前端新手7 小时前
javascript基础- 函数中 this 指向、call、apply、bind
前端·javascript
薛定谔的算法7 小时前
低代码编辑器项目设计与实现:以JSON为核心的数据驱动架构
前端·react.js·前端框架
Hilaku7 小时前
都2025年了,我们还有必要为了兼容性,去写那么多polyfill吗?
前端·javascript·css
yangcode7 小时前
iOS 苹果内购 Storekit 2
前端
LuckySusu7 小时前
【js篇】JavaScript 原型修改 vs 重写:深入理解 constructor的指向问题
前端·javascript
LuckySusu7 小时前
【js篇】如何准确获取对象自身的属性?hasOwnProperty深度解析
前端·javascript