不可变数据 - Immer.js 改造 reducer

immer 官方文档中写✍️:

基本思想是,使用 Immer,会将所有更改应用到临时 draft ,它是 currentState 的代理。一旦完成了所有的 mutationsImmer 将根据对 draft statemutations 生成 nextState。这意味着可以通过简单地修改数据来与数据交互,同时保留不可变数据的所有好处。

使用 Immer 就像拥有一个私人助理。助手拿一封信(当前状态)并给您一份副本(草稿)以记录更改。完成后,助手将接受您的草稿并为您生成真正不变的最终字母(下一个状态)。

核心实现是利用 ES6 的proxy,几乎以最小的成本实现了JavaScript的不可变数据结构,简单易用、体量小巧、设计巧妙,满足了我们对 JS 不可变数据结构的需求。

为什么要追求不可变数据

  1. 性能优化 :不可变数据结构使得在检测数据变化时更加高效。在调和阶段(Reconciliation) React 使用 Virtual DOM 来比较前后两次渲染的虚拟DOM树,找出需要更新的部分,然后只更新这些部分,而不是重新渲染整个DOM。使用不可变数据可以让React在比较时更容易确定哪些数据发生了变化,从而提高性能。

  2. 可预测性:不可变数据使得数据的变化变得可预测。在React中,组件的状态应该是不可变的,这意味着给定一组数据,可以确保它在一段时间内不会改变,这有助于在组件中跟踪数据的状态和变化。

  3. 减少副作用:不可变数据有助于减少副作用。

  4. 更容易调试:不可变数据使得调试更加容易。

  5. 跟踪数据变化:React中的一些生命周期方法和钩子函数依赖于不可变数据,以便正确地触发组件的重新渲染。

基本概念

  • currentState:被操作对象的最初状态
  • draftState: 根据currentState生成的草稿、是currentState的代理、对draftState所有的修改都被记录并用于生成nextState。在此过程中,currentState不受影响
  • nextState: 根据draftState生成的最终状态
  • produce: 用于生成nextState或者producer的函数
  • Producer: 通过produce生成,用于生产nextState,每次执行相同的操作
  • recipe:用于操作draftState的函数

实践

安装

js 复制代码
npm install immer

参考版本号:9.0.19

这里需要配合 @reduxjs/toolkit 使用

ts 复制代码
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { nanoid } from "nanoid";
import produce from "immer";

export type TodoItemType = {
  id: string;
  title: string;
  completed: boolean;
};

const INIT_STATE: TodoItemType[] = [
  { id: nanoid(5), title: "吃饭", completed: false },
  { id: nanoid(5), title: "睡觉", completed: false },
];

export const todoListSlice = createSlice({
  name: "todoList",
  initialState: INIT_STATE,
  reducers: {
    //...
  },
});

export default todoListSlice.reducer;

这是一个 todo 的基础框架。

addTodo

现在需要向 INIT_STATE 中添加数据。实现 addTodo 函数。

ts 复制代码
addTodo(state: TodoItemType[], action: PayloadAction<TodoItemType>) {
  return [
    action.payload,
    ...state,
  ];
},

使用 immer 来实现

ts 复制代码
addTodo: produce(
  (draft: TodoItemType[], action: PayloadAction<TodoItemType>) => {
    draft.unshift(action.payload);
  }
),

removeTodo

移除数组中的某个元素,保证数据不可变,可以使用 array filter

ts 复制代码
removeTodo(state: TodoItemType[], action: PayloadAction<{ id: string }>) {
  const { id: removeId } = action.payload;
  return state.filter((todo) => todo.id !== removeId);
},

使用 immer 来实现

ts 复制代码
removeTodo: produce(
  (draft: TodoItemType[], action: PayloadAction<{ id: string }>) => {
    const { id: removeId } = action.payload;
    const index = draft.findIndex((i) => i.id === removeId);
    draft.splice(index, 1)
  }
),

toggleCompleted

对数组的中的某个元素进行直接的修改

ts 复制代码
toggleCompleted(
  state: TodoItemType[],
  action: PayloadAction<{ id: string }>
) {
  const { id: toggleId } = action.payload;
  return state.map((todo) => {
    const { id, completed } = todo;
    if (id !== toggleId) return todo;
    return {
      ...todo,
      completed: !completed,
    };
  });
},

使用 immer 来实现

ts 复制代码
toggleCompleted: produce(
  (draft: TodoItemType[], action: PayloadAction<{ id: string }>) => {
    const { id: toggleId } = action.payload;
    const todo = draft.find((i) => i.id === toggleId);
    if (todo) {
      todo.completed = !todo.completed;
    }
  }
),

也就是说,基本上可以像操作数组那样方便,但是又可以保证数据的不可变性。

源码

github.com/immerjs/imm...

源码分析文章可阅读:不可变数据实现-Immer.js

相关推荐
outstanding木槿10 分钟前
react+antd的Table组件编辑单元格
前端·javascript·react.js·前端框架
等一场春雨1 小时前
springboot 3 websocket react 系统提示,选手实时数据更新监控
spring boot·websocket·react.js
风无雨1 小时前
react杂乱笔记(一)
前端·笔记·react.js
北海天空2 小时前
reactHooks到底钩到了什么?
前端·react.js
飞翔的渴望2 小时前
react18与react17有哪些区别
前端·javascript·react.js
m0_748238782 小时前
3D架构图软件 iCraft Editor 正式发布 @icraftplayer-react 前端组件, 轻松嵌入3D架构图到您的项目,实现数字孪生
前端·react.js·前端框架
阿卡基YUAN2 小时前
react useCallback
前端·javascript·react.js
米奇妙妙wuu7 小时前
react使用sse流实现chat大模型问答,补充css样式
前端·css·react.js
傻小胖8 小时前
React 生命周期完整指南
前端·react.js
爱喝奶茶的企鹅9 小时前
Next.js 14 性能优化:从首屏加载到运行时优化的最佳实践
react.js