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 对性能影响不大,而且帮助很大应该保留。

相关推荐
并不会1 小时前
常见 CSS 选择器用法
前端·css·学习·html·前端开发·css选择器
衣乌安、1 小时前
【CSS】居中样式
前端·css·css3
兔老大的胡萝卜1 小时前
ppk谈JavaScript,悟透JavaScript,精通CSS高级Web,JavaScript DOM编程艺术,高性能JavaScript pdf
前端·javascript
低代码布道师1 小时前
CSS的三个重点
前端·css
耶啵奶膘2 小时前
uniapp-是否删除
linux·前端·uni-app
王哈哈^_^4 小时前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie4 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic5 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿5 小时前
webWorker基本用法
前端·javascript·vue.js
cy玩具6 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端