Hubleto表单表格数据无法保存?深拷贝完美解决
2025-03-15 13:26:51
表单中表格数据无法保存问题的排查与解决
最近上手试用了 Hubleto 的新版本,想用一下它的表单功能。但后来发现,表单里表格的数据没法保存。
我研究了一下代码,发现在 Form.tsx
文件里的 saveRecord()
方法中,关系数据(一个对象数组)在被转移到 record
变量时,数据就没了。
数据结构大概长这样:
{
id: 1,
PRODUCTS: [
{id: 1},
{id: 2}
]
}
问题出在这行代码:
let record = { ...this.state.record, id: this.state.id };
不知道是啥原因导致的? 下面我们来深入分析和解决。
一、 问题原因分析
看到这行代码 let record = { ...this.state.record, id: this.state.id };
, 我们可以初步判断,问题很可能出在 JavaScript 的浅拷贝机制上。
展开运算符 (...
) 对对象进行的是浅拷贝。 啥意思呢? 简单说,就是如果对象内部还有对象(像你例子里的 PRODUCTS
数组,数组里每个元素又是一个对象),那么浅拷贝只会复制这些内部对象的引用,而不是创建一个全新的对象。
所以,record
和 this.state.record
里的 PRODUCTS
属性,其实指向的是同一个数组。之后如果对 record
或 this.state.record
中的 PRODUCTS
进行修改(比如删除、添加元素),另一个也会跟着变。 在你遇到的场景里, 推测是在后续的操作中,record
的PRODUCTS
被不小心修改掉了。
二、 解决方案
既然知道是浅拷贝惹的祸,那解决办法就有了:想办法进行深拷贝,让 record
拥有一个独立的 PRODUCTS
数组。下面提供几种方案。
1. 使用 JSON.parse(JSON.stringify())
这是最简单粗暴的方法,直接把对象序列化成 JSON 字符串,再解析回来,就得到一个全新的对象了。
-
原理:
JSON.stringify()
将 JavaScript 对象转换为 JSON 字符串。
JSON.parse()
将 JSON 字符串转换回 JavaScript 对象。
这个过程会创建一个全新的对象,切断与原对象的所有引用关系。 -
代码示例:
let record = JSON.parse(JSON.stringify(this.state.record));
record.id = this.state.id;
- 注意事项:
- 这种方法简单,但有局限。如果对象里有函数、
undefined
、Symbol
、循环引用等,JSON.stringify()
会把它们忽略掉或报错。 - 如果对象非常大,这种方法可能会有效率问题,毕竟多了序列化和反序列化的操作。
- 这种方法简单,但有局限。如果对象里有函数、
2. 使用 Lodash 的 cloneDeep
方法
Lodash 是一个流行的 JavaScript 工具库,提供了很多实用的函数。cloneDeep
就是其中一个,专门用来深拷贝对象。
-
原理:
cloneDeep
会递归地复制对象的所有属性,包括嵌套的对象和数组,确保得到一个完全独立的副本。 -
代码示例:
// 首先,确保安装了 Lodash: npm install lodash
import cloneDeep from 'lodash/cloneDeep';
// ...
let record = cloneDeep(this.state.record);
record.id = this.state.id;
- 注意事项:
- 需要先安装 Lodash 库。
- 引入的时候按需引入,
import cloneDeep from 'lodash/cloneDeep';
,不要全部引入,减小打包体积。
3. 手动实现深拷贝 (递归)
如果不想引入外部库,或者想更深入地理解深拷贝,也可以自己写一个递归函数来实现。
-
原理
通过遍历,将对象的每一个层级进行复制. -
代码示例:
function deepClone(obj: any): any {
if (obj === null || typeof obj !== "object") {
return obj;
}
let cloned: any = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
cloned[key] = deepClone(obj[key]);
}
}
return cloned;
}
// ...
let record = deepClone(this.state.record);
record.id = this.state.id;
- 注意事项:
- 这段代码只是一个基础版本,没有处理循环引用、函数等特殊情况。如果需要处理这些情况,代码会更复杂。
- 对性能要求较高的话,可能需要进一步优化。
4. 使用 structuredClone (如果环境支持)
structuredClone
是一个较新的 Web API,可以直接用来深拷贝对象。 它的性能通常优于 JSON.parse(JSON.stringify())
,并且能处理更多的数据类型。
-
原理:
structuredClone
使用结构化克隆算法, 直接复制值, 对性能进行了优化。 -
代码示例:
let record = structuredClone(this.state.record);
record.id = this.state.id;
- 注意事项:
- 这个 API 比较新,老旧浏览器可能不支持。需要检查一下你的目标用户使用的浏览器版本,确保兼容性。可以使用 caniuse.com 来查看浏览器支持情况。
- 不能克隆函数和 Error 对象.
三、进阶:排查修改源
上面提供的几种方案,都可以解决数据丢失的问题。但更重要的是,找到后续代码中哪里修改了 PRODUCTS
数组。
可以在 saveRecord()
方法的后面,以及其他可能操作 record
或 this.state.record
的地方,打上断点,或者使用 console.log()
输出,观察 PRODUCTS
数组的变化,从而定位到问题代码。
可以观察修改前和修改后的值,确定问题代码的具体位置。
比如:
saveRecord() {
let record = JSON.parse(JSON.stringify(this.state.record));
record.id = this.state.id;
console.log("before modify",record.PRODUCTS) // 增加这一行代码
// .... 其他的代码
console.log("after modify",record.PRODUCTS) // 增加这一行代码
}
定位并修改导致PRODUCTS
被错误修改的代码. 这才是治本之策.
四. 额外的安全建议
虽然这和此次遇到的bug没有直接关系,但是培养良好的编码习惯对减少类似的问题,以及代码的安全性都非常有必要.
-
数据不可变性 (Immutability): 尽可能使用不可变数据。如果你的状态管理库(比如 Redux、Zustand)支持不可变数据,尽量使用它们提供的更新状态的方式,而不是直接修改对象。这可以避免很多意料之外的副作用。
-
类型检查: 尽可能的使用typescript来书写, typescript可以在编写期间就发现很多的潜在错误。
-
彻底的测试: 对表单部分增加单元测试和集成的测试。保证后续对表单修改, 不会引入其他bug.