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

相关推荐
小李小李不讲道理5 小时前
「Ant Design 组件库探索」四:Input组件
前端·javascript·react.js
知识分享小能手12 小时前
React学习教程,从入门到精通,React AJAX 语法知识点与案例详解(18)
前端·javascript·vue.js·学习·react.js·ajax·vue3
NeverSettle_17 小时前
React工程实践面试题深度分析2025
javascript·react.js
学前端搞口饭吃17 小时前
react reducx的使用
前端·react.js·前端框架
努力往上爬de蜗牛17 小时前
react3面试题
javascript·react.js·面试
开心不就得了17 小时前
React 进阶
前端·javascript·react.js
谢尔登17 小时前
【React】React 哲学
前端·react.js·前端框架
学前端搞口饭吃20 小时前
react context如何使用
前端·javascript·react.js
GDAL20 小时前
为什么Cesium不使用vue或者react,而是 保留 Knockout
前端·vue.js·react.js
Dragon Wu1 天前
React state在setInterval里未获取最新值的问题
前端·javascript·react.js·前端框架