不可变数据 - 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

相关推荐
GISer_Jing16 小时前
React核心功能详解(一)
前端·react.js·前端框架
FØund40418 小时前
antd form.setFieldsValue问题总结
前端·react.js·typescript·html
疯狂的沙粒19 小时前
如何在 React 项目中应用 TypeScript?应该注意那些点?结合实际项目示例及代码进行讲解!
react.js·typescript
鑫宝Code20 小时前
【React】React Router:深入理解前端路由的工作原理
前端·react.js·前端框架
沉默璇年1 天前
react中useMemo的使用场景
前端·react.js·前端框架
红绿鲤鱼1 天前
React-自定义Hook与逻辑共享
前端·react.js·前端框架
loey_ln1 天前
FIber + webWorker
javascript·react.js
zhenryx1 天前
前端-react(class组件和Hooks)
前端·react.js·前端框架
water1 天前
Nextjs系列——新版本路由的使用
前端·javascript·react.js
老码沉思录2 天前
React Native 全栈开发实战班 - 性能与调试之打包与发布
javascript·react native·react.js