返回

Hubleto表单表格数据无法保存?深拷贝完美解决

php

表单中表格数据无法保存问题的排查与解决

最近上手试用了 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 数组,数组里每个元素又是一个对象),那么浅拷贝只会复制这些内部对象的引用,而不是创建一个全新的对象。

所以,recordthis.state.record 里的 PRODUCTS 属性,其实指向的是同一个数组。之后如果对 recordthis.state.record 中的 PRODUCTS 进行修改(比如删除、添加元素),另一个也会跟着变。 在你遇到的场景里, 推测是在后续的操作中,recordPRODUCTS被不小心修改掉了。

二、 解决方案

既然知道是浅拷贝惹的祸,那解决办法就有了:想办法进行深拷贝,让 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;
  • 注意事项:
    • 这种方法简单,但有局限。如果对象里有函数、undefinedSymbol、循环引用等,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() 方法的后面,以及其他可能操作 recordthis.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没有直接关系,但是培养良好的编码习惯对减少类似的问题,以及代码的安全性都非常有必要.

  1. 数据不可变性 (Immutability): 尽可能使用不可变数据。如果你的状态管理库(比如 Redux、Zustand)支持不可变数据,尽量使用它们提供的更新状态的方式,而不是直接修改对象。这可以避免很多意料之外的副作用。

  2. 类型检查: 尽可能的使用typescript来书写, typescript可以在编写期间就发现很多的潜在错误。

  3. 彻底的测试: 对表单部分增加单元测试和集成的测试。保证后续对表单修改, 不会引入其他bug.