immer 官方文档中写✍️:
基本思想是,使用 Immer,会将所有更改应用到临时 draft ,它是 currentState 的代理。一旦完成了所有的 mutations ,Immer 将根据对 draft state 的 mutations 生成 nextState。这意味着可以通过简单地修改数据来与数据交互,同时保留不可变数据的所有好处。
使用 Immer 就像拥有一个私人助理。助手拿一封信(当前状态)并给您一份副本(草稿)以记录更改。完成后,助手将接受您的草稿并为您生成真正不变的最终字母(下一个状态)。
核心实现是利用 ES6 的proxy
,几乎以最小的成本实现了JavaScript
的不可变数据结构,简单易用、体量小巧、设计巧妙,满足了我们对 JS 不可变数据结构的需求。
为什么要追求不可变数据
-
性能优化 :不可变数据结构使得在检测数据变化时更加高效。在调和阶段(Reconciliation) React 使用 Virtual DOM 来比较前后两次渲染的虚拟DOM树,找出需要更新的部分,然后只更新这些部分,而不是重新渲染整个DOM。使用不可变数据可以让React在比较时更容易确定哪些数据发生了变化,从而提高性能。
-
可预测性:不可变数据使得数据的变化变得可预测。在React中,组件的状态应该是不可变的,这意味着给定一组数据,可以确保它在一段时间内不会改变,这有助于在组件中跟踪数据的状态和变化。
-
减少副作用:不可变数据有助于减少副作用。
-
更容易调试:不可变数据使得调试更加容易。
-
跟踪数据变化: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;
}
}
),
也就是说,基本上可以像操作数组那样方便,但是又可以保证数据的不可变性。
源码
源码分析文章可阅读:不可变数据实现-Immer.js