在 React、Redux 等前端框架的开发中,"状态不可变"是核心原则------直接修改原始状态会导致组件无法正常重渲染、状态追踪失效等问题。但原生 JavaScript 操作嵌套层级较深的不可变数据时,需要逐层拷贝,代码冗余且易出错。
Immer 作为一款轻量 JavaScript 库,恰好解决了这一痛点。它的核心 API produce 允许我们用"可变的写法"修改"草稿对象(draft)",底层会自动追踪变化、生成全新的不可变数据,实现"写时可变,读时不可变"。本文将通过四大高频实战案例,带你吃透 Immer 的用法,感受其核心优势。
一、核心前提:Immer 核心 API 简介
Immer 的核心能力集中在 produce 方法上,语法简洁且灵活,主要有两种常用形式:
ini
import { produce } from "immer";
// 形式1:直接传入原始数据和修改函数
const newData = produce(originalData, draft => {
// 直接修改 draft(草稿),无需手动拷贝
draft.xxx = 新值;
});
// 形式2:柯里化(先定义修改逻辑,后续传入原始数据)
const updateFn = produce((draft, payload) => {
draft.xxx = payload; // 可接收额外参数
});
const newData = updateFn(originalData, 新值);
关键说明:originalData 是原始不可变数据,draft 是 Immer 生成的代理对象(可直接修改),newData 是最终生成的全新不可变数据,原始数据始终保持不变。
二、实战案例:Immer 核心用法落地
以下案例均采用"原生写法 vs Immer 写法"对比形式,直观体现 Immer 简化代码、降低心智负担的优势。
案例1:修改嵌套对象(高频场景)
场景:有一个多层嵌套的用户信息对象,需要修改深层的"年龄"字段,同时修改外层的"姓名"字段。
原生写法(繁琐且易出错)
原生操作需逐层用扩展运算符(...)拷贝,嵌套越深,拷贝层级越多,代码冗余度呈指数级增加:
arduino
// 原始数据
const oldState = {
user: {
name: "张三",
info: { age: 20, address: "北京", contact: { phone: "13800138000" } }
}
};
// 修改:姓名改为"李四",年龄改为21
const newState = {
...oldState, // 拷贝最外层对象
user: {
...oldState.user, // 拷贝 user 层
name: "李四", // 修改姓名
info: {
...oldState.user.info, // 拷贝 info 层
age: 21, // 修改年龄
contact: {
...oldState.user.info.contact // 若需修改 phone,需再拷贝 contact 层
}
}
}
};
console.log(oldState.user.info.age); // 20(原始数据不变)
console.log(newState.user.info.age); // 21(新数据生效)
Immer 写法(简洁直观)
无需手动逐层拷贝,直接定位到目标字段修改即可,代码量大幅减少,可读性提升:
ini
import { produce } from "immer";
// 原始数据(同原生案例)
const oldState = {
user: {
name: "张三",
info: { age: 20, address: "北京", contact: { phone: "13800138000" } }
}
};
// Immer 修改逻辑
const newState = produce(oldState, draft => {
draft.user.name = "李四"; // 直接修改外层字段
draft.user.info.age = 21; // 直接修改深层字段
// 若需修改 phone,直接写 draft.user.info.contact.phone = "13900139000" 即可
});
console.log(oldState.user.info.age); // 20(原始数据不变)
console.log(newState.user.info.age); // 21(新数据生效)
案例2:操作数组(增删改查)
场景:有一个任务列表数组,需要完成"标记指定任务为完成""新增任务""删除任务"三个操作。
原生写法(需手动生成新数组)
数组的 push、splice 等方法会修改原数组,因此需用 map、filter 等方法生成新数组,操作繁琐:
ini
// 原始任务列表
const oldList = [
{ id: 1, title: "学习 Immer", done: false },
{ id: 2, title: "写实战案例", done: false },
{ id: 3, title: "整理笔记", done: false }
];
// 需求1:标记 id=2 的任务为完成
// 需求2:新增 id=4 的任务
// 需求3:删除 id=3 的任务
const newList = oldList
.map(item => item.id === 2 ? { ...item, done: true } : item) // 标记完成(生成新对象)
.concat({ id: 4, title: "分享文章", done: false }) // 新增任务(生成新数组)
.filter(item => item.id !== 3); // 删除任务(生成新数组)
console.log(oldList.length); // 3(原始数组不变)
console.log(newList.length); // 3(新数组:id=1、2、4)
Immer 写法(直接操作数组)
可直接使用 push、splice 等"可变"方法操作 draft 数组,Immer 会自动转换为不可变操作:
ini
import { produce } from "immer";
// 原始任务列表(同原生案例)
const oldList = [
{ id: 1, title: "学习 Immer", done: false },
{ id: 2, title: "写实战案例", done: false },
{ id: 3, title: "整理笔记", done: false }
];
// Immer 操作数组
const newList = produce(oldList, draft => {
// 1. 标记 id=2 的任务为完成
const targetItem = draft.find(item => item.id === 2);
if (targetItem) targetItem.done = true;
// 2. 新增任务(直接 push)
draft.push({ id: 4, title: "分享文章", done: false });
// 3. 删除 id=3 的任务(直接 splice)
const deleteIndex = draft.findIndex(item => item.id === 3);
if (deleteIndex !== -1) draft.splice(deleteIndex, 1);
});
console.log(oldList.length); // 3(原始数组不变)
console.log(newList.length); // 3(新数组:id=1、2、4)
案例3:结合 React useState(状态管理)
场景:React 组件中,状态是嵌套对象,需要修改深层字段(如用户年龄),同时保证状态不可变。
原生写法(需手动拷贝状态)
修改状态时需返回全新对象,嵌套层级深时,拷贝逻辑繁琐且易遗漏:
javascript
import { useState } from "react";
function UserInfo() {
// 嵌套状态
const [state, setState] = useState({
user: { name: "张三", info: { age: 20, gender: "男" } }
});
// 年龄+1 操作
const increaseAge = () => {
setState(prevState => ({
...prevState,
user: {
...prevState.user,
info: {
...prevState.user.info,
age: prevState.user.info.age + 1
}
}
}));
};
return (
<div>
<p>姓名:{state.user.name}</p>
<p>年龄:{state.user.info.age}</p>
<button onClick={increaseAge}>年龄+1</button>
</div>
);
}
Immer 写法(简化状态修改)
无需手动拷贝 prevState,直接修改 draft 即可,代码更简洁,心智负担更低:
javascript
import { useState } from "react";
import { produce } from "immer";
function UserInfo() {
// 嵌套状态(同原生案例)
const [state, setState] = useState({
user: { name: "张三", info: { age: 20, gender: "男" } }
});
// 年龄+1 操作(Immer 简化)
const increaseAge = () => {
setState(produce(draft => {
// 直接修改深层字段,无需拷贝
draft.user.info.age += 1;
}));
};
return (
<div>
<p>姓名:{state.user.name}</p>
<p>年龄:{state.user.info.age}</p>
<button onClick={increaseAge}>年龄+1</button>
</div>
);
}
案例4:结合 Redux Reducer(纯函数优化)
场景:Redux 的 Reducer 是纯函数,必须返回全新状态,不能修改原状态。嵌套状态下,原生写法冗余度极高。
原生写法(多层拷贝,代码冗余)
php
// 初始状态
const initialState = {
user: { name: "张三", info: { age: 20, address: "北京" } },
token: ""
};
// 原生 Reducer
function userReducer(state = initialState, action) {
switch (action.type) {
case "UPDATE_AGE":
// 逐层拷贝,修改年龄
return {
...state,
user: {
...state.user,
info: {
...state.user.info,
age: action.payload
}
}
};
case "SET_TOKEN":
// 修改 token(单层,相对简单)
return { ...state, token: action.payload };
default:
return state;
}
}
Immer 写法(简化 Reducer 逻辑)
用 Immer 包裹 Reducer,直接修改 draft 状态,无需手动拷贝,Reducer 逻辑更清晰:
javascript
import { produce } from "immer";
// 初始状态(同原生案例)
const initialState = {
user: { name: "张三", info: { age: 20, address: "北京" } },
token: ""
};
// Immer 简化 Reducer
const userReducer = produce((draft, action) => {
switch (action.type) {
case "UPDATE_AGE":
// 直接修改深层年龄字段
draft.user.info.age = action.payload;
break;
case "SET_TOKEN":
// 直接修改 token
draft.token = action.payload;
break;
}
}, initialState); // 第二个参数为初始状态
三、Immer 核心优势总结
通过以上案例对比,Immer 的核心优势的可概括为三点,每一点都直击原生不可变操作的痛点:
1. 降低心智负担,代码更简洁
无需记忆"逐层拷贝"规则,不用写大量扩展运算符(...),直接用"可变写法"操作数据,嵌套层级越深,优势越明显。尤其在 Redux Reducer、React 嵌套状态修改中,能大幅减少冗余代码。
2. 减少出错概率,提升开发效率
原生操作中,嵌套层级深时容易遗漏拷贝层级,导致状态修改异常;Immer 自动处理拷贝逻辑,只需关注"要修改什么",无需关注"如何拷贝",降低出错概率,提升开发效率。
3. 性能更优,支持结构共享
Immer 并非深拷贝(JSON.parse(JSON.stringify)),而是采用"结构共享"机制------只拷贝发生变化的层级,未修改的层级直接复用原始数据,比深拷贝更高效,尤其适合大数据量场景。
四、总结
Immer 以"简单、高效、低心智负担"为核心,通过 produce API 完美解决了 JavaScript 不可变数据操作的痛点。无论是嵌套对象、数组的日常操作,还是 React、Redux 中的状态管理,Immer 都能让代码更简洁、易维护。
核心记住一句话:用 Immer,你只管"修改"草稿(draft),剩下的不可变逻辑,它来帮你搞定。