欢迎光临
我们一直在努力

React 状态拷贝完全指南:别再无脑用 _.cloneDeep 了

导读

在 React 日常开发中,状态拷贝是高频操作。本文将系统梳理三种拷贝方式的底层机制、适用场景、性能差异及决策依据,帮助你建立清晰的选型判断体系。

一、深拷贝与浅拷贝:概念与内存模型

核心定义

拷贝方式实现方式机制描述
浅拷贝 […arr] / {…obj} / Object.assign() 仅复制最外层容器,内部元素共享原引用
深拷贝 _.cloneDeep / JSON.parse(JSON.stringify()) 递归复制所有层级,生成完全独立的副本

内存模型图解

以对象数组为例:

const original = [{ id: 1 }, { id: 2 }];

浅拷贝 […original]:

original → [ 0x001 ] → { id: 1 } ← 两个数组元素
[ 0x002 ] → { id: 2 } ← 指向同一对象
shallow → [ 0x001 ] → { id: 1 }
[ 0x002 ] → { id: 2 }

修改 shallow[0].id 会同步影响 original[0].id。

深拷贝 _.cloneDeep(original):

original → [ 0x001 ] → { id: 1 }
[ 0x002 ] → { id: 2 }
deep → [ 0x003 ] → { id: 1 } ← 完全独立的内存空间
[ 0x004 ] → { id: 2 }

修改 deep[0].id 不影响 original[0].id。

当数组元素为基本类型(string/number)时,深浅拷贝效果一致,因为基本类型值不可变。

二、React 状态更新中的三种拷贝方案

方案一:浅拷贝(Spread 语法)

适用场景:

  • 状态为基本类型数组(如 ID 列表)
  • 对象数组仅需增删元素,不修改内部对象属性
  • 嵌套层级 ≤ 2 层

代码示例:

// 基本类型数组 —— 增删改均安全
const newKeys = [(this.state.selectedRowKeys || [])];
newKeys.push('P004');
newKeys.pop();
newKeys.splice(1, 1);
this.setState({ selectedRowKeys: newKeys });

// 对象数组 —— 仅整体替换时安全
const newRows = [(this.state.selectedRows || [])];
newRows.push(newRecord); // 安全
newRows = newRows.filter(item => item.id !== targetId); // 安全

// 对象数组 —— 修改内部属性需特别处理
const newRows = this.state.rows.map(item =>
item.id === targetId ? { item, status: 'updated' } : item
);
this.setState({ rows: newRows });

性能特征:O(n) 时间复杂度,仅复制数组外壳,1000 个元素耗时约 0.01ms。

方案二:深拷贝(_.cloneDeep)

适用场景:

  • 需要完全独立副本的模板/配置克隆
  • 撤销/重做(Undo/Redo)历史快照
  • 深拷贝后不再与源数据做引用比对

代码示例:

import _ from 'lodash';

// 模板克隆 —— 编辑不影响原始模板
const template = { layout: { columns: 3 }, fields: [] };
const editable = _.cloneDeep(template);
editable.layout.columns = 4; // 不影响 template

// 历史快照
historyStack.push(_.cloneDeep(currentState));

⚠️ 风险提示:

在 selectedRows 场景中使用 _.cloneDeep 会导致引用混杂:

// ❌ 问题代码
const patientA = { id: 'P001' };
this.state = { selectedRows: [patientA] };

const newRows = _.cloneDeep(this.state.selectedRows);
newRows.push(patientB);
// newRows[0] 是 patientA 的副本(新地址)
// newRows[1] 是 patientB 的原引用(原地址)
// 引用不一致,后续 === 比对失效

性能特征:递归遍历整个树结构,1000 个对象(10属性/个)耗时约 3~5ms。

方案三:Immer(推荐用于复杂嵌套)

Immer 是 React/JS 生态中最流行的不可变状态管理库,被 Redux Toolkit、Zustand 等现代状态库内置使用。其核心机制是利用 Proxy 实现写时复制(Copy-on-Write)。

适用场景:

  • 嵌套层级 ≥ 3 层的复杂 State
  • 需要高频更新深层属性的场景
  • 追求代码可读性和维护性的项目

代码示例:

import produce from 'immer';

// 三层嵌套修改 —— 3 行搞定
this.setState(
produce(draft => {
draft.user.info.address.city = 'Hangzhou';
draft.selectedRowKeys.push('P003');
})
);

// 对比原生展开写法 —— 13 行且易出错
this.setState({
user: {
this.state.user,
info: {
this.state.user.info,
address: {
this.state.user.info.address,
city: 'Hangzhou'
}
}
}
});

底层原理:

Immer 使用 ES6 Proxy 对传入状态进行代理:

  • 读取操作:无性能开销,直接返回原值
  • 修改操作:仅复制从根节点到修改目标路径上的节点,未修改的分支保持原引用不变

这意味着只有真正变化的数据路径会触发组件重渲染,性能表现优于全量深拷贝。

三、方案对比总览

维度浅拷贝 […]深拷贝 _.cloneDeepImmer produce
编写体验 浅层简洁,深层繁琐 简单直接 极度舒适(命令式语法)
嵌套深层修改 需逐层展开,易出错 ✅ 直接修改 ✅ 直接修改
未修改节点引用 ✅ 保持原引用 ❌ 全部重建 ✅ 仅修改路径重建
性能开销 极低(~0.01ms) 较高(3~5ms) 中等(0.5~1ms)
学习成本 需了解 produce/Proxy 概念
适用场景 浅层状态、基本类型数组 模板克隆、历史快照 3层以上嵌套 State

四、选型决策树

需要拷贝状态

├── 数据是基本类型数组(string/number)?
│ └── 是 → 浅拷贝 […arr] ✅

├── 数据是对象数组,仅增删元素,不改内部属性?
│ └── 是 → 浅拷贝 […arr] ✅

├── 数据是深层嵌套对象(≥3层)?
│ └── 是 → Immer produce ✅

└── 需要完全独立副本,彻底解耦(模板克隆/历史记录)?
└── 是 → _.cloneDeep(需评估引用比对风险)⚠️

五、性能量化参考

场景浅拷贝 […]_.cloneDeepImmer produce
1000 个数字 ~0.01ms ~0.02ms N/A
1000 个对象(10属性/个) ~0.01ms 35ms 0.51ms
3层嵌套改深层属性 需手写展开 35ms(全量重建) 0.51ms(仅重建修改路径)

六、最佳实践总结

状态类型推荐方案原因
selectedRowKeys(ID 列表) 浅拷贝 […] 基本类型,浅拷贝已满足不可变要求
selectedRows(对象数组) 浅拷贝 […] 保持元素引用一致,确保 === 比对有效
深层嵌套 State(≥3层) Immer produce 代码简洁,性能优于全量深拷贝
模板/配置克隆 _.cloneDeep 需要完全独立的副本
历史快照 _.cloneDeep 需要某一时刻的完整状态备份
通用推荐 Immer 统一处理复杂场景 兼顾可读性、正确性和性能

核心原则

  • 浅拷贝是正确场景下的最优解,不是偷懒
  • 深拷贝不是万能药,滥用会导致引用混乱和性能问题
  • Immer 让复杂状态更新变得简单,是现代 React 的标配工具
  • 赞(0)
    未经允许不得转载:171主机测评 » React 状态拷贝完全指南:别再无脑用 _.cloneDeep 了
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址