介绍
Immer 简化了不可变数据结构的处理, Immer 将通过解决以下痛点来帮助您遵循不可变数据范式:
- Immer 将检测到意外 mutations 并抛出错误。
- Immer 将不再需要创建对不可变对象进行深度更新时所需的典型样板代码:如果没有 Immer,则需要在每个级别手动制作对象副本。通常通过使用大量
...展开操作。使用 Immer 时,会对draft对象进行更改,该对象会记录更改并负责创建必要的副本,而不会影响原始对象。 - 使用 Immer 时,您无需学习专用 API 或数据结构即可从范例中受益。使用 Immer,您将使用纯 JavaScript 数据结构,并使用众所周知的安全地可变 JavaScript API。
一个简单的比较示例
const baseState = [
{
title: "Learn TypeScript",
done: true
},
{
title: "Try Immer",
done: false
}
]不使用 Immer
如果没有 Immer,我们将不得不小心地浅拷贝每层受我们更改影响的 state 结构
const nextState = baseState.slice() // 浅拷贝数组
nextState[1] = {
// 替换第一层元素
...nextState[1], // 浅拷贝第一层元素
done: true // 期望的更新
}
// 因为 nextState 是新拷贝的, 所以使用 push 方法是安全的,
// 但是在未来的任意时间做相同的事情会违反不变性原则并且导致 bug!
nextState.push({title: "Tweet about it"})使用 Immer
使用 Immer,这个过程更加简单。我们可以利用 produce 函数,它将我们要更改的 state 作为第一个参数,对于第二个参数,我们传递一个名为 recipe 的函数,该函数传递一个 draft 参数,我们可以对其应用直接的 mutations。一旦 recipe 执行完成,这些 mutations 被记录并用于产生下一个状态。 produce 将负责所有必要的复制,并通过冻结数据来防止未来的意外修改。
import {produce} from "immer"
const nextState = produce(baseState, draft => {
draft[1].done = true
draft.push({title: "Tweet about it"})
})Immer 如何工作
基本思想是,使用 Immer,您会将所有更改应用到临时 draft,它是 currentState 的代理。一旦你完成了所有的 mutations,Immer 将根据对 draft state 的 mutations 生成 nextState。这意味着您可以通过简单地修改数据来与数据交互,同时保留不可变数据的所有好处。

使用 Immer 就像拥有一个私人助理。助手拿一封信(当前状态)并给您一份副本(草稿)以记录更改。完成后,助手将接受您的草稿并为您生成真正不变的最终字母(下一个状态)。
原理
mmer 的核心原理是利用 ES6 Proxy 来实现 Copy-on-Write (写时复制) 机制。
它让你以 “可变(Mutable)” 的语法(比如 state.count = 1)来操作数据,但最终自动生成一个 “不可变(Immutable)” 的新状态对象。
- 核心流程:三态生命周期
Immer 的工作流程可以概括为三个阶段:
Current State(当前状态):作为输入传递给 Immer 的原始不可变对象。
Draft State(草稿状态):Immer 创建的一个 Proxy 代理对象。它看起来和 Current State 一模一样,但你可以随意修改它。
Next State(下一个状态):当你修改完 Draft 后,Immer 会根据你的修改记录,生成一个新的不可变对象。
步骤一:创建 Proxy(拦截修改)
Immer 不需要深拷贝整个对象(那样太慢了)。它只是创建了一层薄薄的 Proxy。
步骤二:生成最终状态 (Finalize)
当你结束修改后,Immer 会检查:
如果没修改过 (modified === false) -> 直接返回原始对象(结构共享)。
如果修改过 -> 返回那个副本 (copy)。
步骤三:结构共享 (Structural Sharing)
这是 Immer 高效的关键。没有被修改的节点,新旧对象会共享同一个引用。
使用 produce
Immer 包暴露了一个完成所有工作的默认函数。
produce(currentState, recipe: (draftState) => void): nextStateTIP
请注意,recipe 函数通常不会返回任何内容。但是,如果您想用另一个对象完全替换 draft,则可以返回,
柯里化 producers
将函数作为第一个参数传递给 produce 会创建一个函数,该函数尚未将 produce 应用于特定 state,而是创建一个函数,该函数将应用于将来传递给它的任何 state。这通常称为柯里化。举个例子:
import {produce} from "immer"
function toggleTodo(state, id) {
return produce(state, draft => {
const todo = draft.find(todo => todo.id === id)
todo.done = !todo.done
})
}
const baseState = [
{
id: "JavaScript",
title: "Learn TypeScript",
done: true
},
{
id: "Immer",
title: "Try Immer",
done: false
}
]
const nextState = toggleTodo(baseState, "Immer")上面的 toggleTodo 模式非常典型;传递一个现有的 state 来 produce,修改 draft,然后返回结果。由于 state 除了将其传递给 produce 之外没有其他任何用途,因此可以通过使用 produce 的柯里化形式来简化上面的示例,其中您只传递 produce recipe 函数,并且 produce 将返回一个应用 recipe 到基础状态的新函数。这允许我们缩短上述 toggleTodo 定义。
import {produce} from "immer"
// curried producer:
const toggleTodo = produce((draft, id) => {
const todo = draft.find(todo => todo.id === id)
todo.done = !todo.done
})
const baseState = [
/* as is */
]
const nextState = toggleTodo(baseState, "Immer")请注意,id 参数现在已成为 recipe 函数的一部分!这种拥有 curried producers 的模式与 React 中的 useState Hook 非常巧妙地结合在一起
React & Immer
useStatehook 假定存储在其中的任何 state 都被视为不可变的。使用 Immer 可以大大简化 React 组件状态的深度更新。下面的例子展示了如何使用produce和useState
import React, { useCallback, useState } from "react";
import {produce} from "immer";
const TodoList = () => {
const [todos, setTodos] = useState([
{
id: "React",
title: "Learn React",
done: true
},
{
id: "Immer",
title: "Try Immer",
done: false
}
]);
const handleToggle = useCallback((id) => {
setTodos(
produce((draft) => {
const todo = draft.find((todo) => todo.id === id);
todo.done = !todo.done;
})
);
}, []);
const handleAdd = useCallback(() => {
setTodos(
produce((draft) => {
draft.push({
id: "todo_" + Math.random(),
title: "A new todo",
done: false
});
})
);
}, []);
return (<div>{*/ See CodeSandbox */}</div>)
}useImmer
由于所有 state 的更新都使用 produce 包装的更新模式,所以我们可以通过将更新模式包装在 use-immer 包中来简化上述操作
import React, { useCallback } from "react";
import { useImmer } from "use-immer";
const TodoList = () => {
const [todos, setTodos] = useImmer([
{
id: "React",
title: "Learn React",
done: true
},
{
id: "Immer",
title: "Try Immer",
done: false
}
]);
const handleToggle = useCallback((id) => {
setTodos((draft) => {
const todo = draft.find((todo) => todo.id === id);
todo.done = !todo.done;
});
}, []);
const handleAdd = useCallback(() => {
setTodos((draft) => {
draft.push({
id: "todo_" + Math.random(),
title: "A new todo",
done: false
});
});
}, []);
// etcuseReducer + Immer
import React, {useCallback, useReducer} from "react"
import {produce} from "immer"
const TodoList = () => {
const [todos, dispatch] = useReducer(
produce((draft, action) => {
switch (action.type) {
case "toggle":
const todo = draft.find(todo => todo.id === action.id)
todo.done = !todo.done
break
case "add":
draft.push({
id: action.id,
title: "A new todo",
done: false
})
break
default:
break
}
}),
[
/* initial todos */
]
)
const handleToggle = useCallback(id => {
dispatch({
type: "toggle",
id
})
}, [])
const handleAdd = useCallback(() => {
dispatch({
type: "add",
id: "todo_" + Math.random()
})
}, [])
// etc
}useImmerReducer
同上,可以通过 use-immer 包中的 useImmerReducer 简化
import React, { useCallback } from "react";
import { useImmerReducer } from "use-immer";
const TodoList = () => {
const [todos, dispatch] = useImmerReducer(
(draft, action) => {
switch (action.type) {
case "toggle":
const todo = draft.find((todo) => todo.id === action.id);
todo.done = !todo.done;
break;
case "add":
draft.push({
id: action.id,
title: "A new todo",
done: false
});
break;
default:
break;
}
},
[ /* initial todos */ ]
);
//etc