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

相关推荐
zqx_714 小时前
随记 前端框架React的初步认识
前端·react.js·前端框架
TonyH20021 天前
webpack 4 的 30 个步骤构建 react 开发环境
前端·css·react.js·webpack·postcss·打包
掘金泥石流1 天前
React v19 的 React Complier 是如何优化 React 组件的,看 AI 是如何回答的
javascript·人工智能·react.js
lucifer3111 天前
深入解析 React 组件封装 —— 从业务需求到性能优化
前端·react.js
秃头女孩y2 天前
React基础-快速梳理
前端·react.js·前端框架
sophie旭2 天前
我要拿捏 react 系列二: React 架构设计
javascript·react.js·前端框架
BHDDGT2 天前
react-问卷星项目(5)
前端·javascript·react.js
liangshanbo12152 天前
将 Intersection Observer 与自定义 React Hook 结合使用
前端·react.js·前端框架
黄毛火烧雪下2 天前
React返回上一个页面,会重新挂载吗
前端·javascript·react.js