目录

如何解决ForEach渲染对象数组时,删除数据异常的问题

问题现象

ForEach渲染对象数组时,删除第三项ListItem时,剩余的ListItem会出现“选项6”重复显示的问题。问题代码如下:

class TestItem {
  id: number = 0;
  content: string = '';
}


@Entry
@Component
struct ListTestPage {
  @State testArray: Array<TestItem> = [];


  // 删除数组项
  changeArray(testArray: Array<TestItem>, index: number): Array<TestItem> {
    if (testArray.length <= 0 || index < 0 || index >= testArray.length) {
      return testArray;
    }
    return testArray.filter((_, i) => i !== index);
  }


  build() {
    Column() {
      Text('测试List复用异常的问题')
        .fontColor(Color.Black)
        .fontSize(18);
      List({ space: 8 }) {
        ForEach(this.testArray, (itemBean: TestItem, index: number) => {
          ListItem() {
            this.builderVoteOpinionView(itemBean, index);
          };
        });
      }
      .width('90%')
      .enableScrollInteraction(false)
      .scrollBar(BarState.Off)
      .edgeEffect(EdgeEffect.None)
      .margin({ top: 30 });


      // 添加按钮
      Row() {
        Button('添加列表数量');
      }
      .margin({ top: 20 })
      .borderRadius(4)
      .alignItems(VerticalAlign.Center)
      .onClick(() => {
        this.testArray.push(new TestItem());
      });
    }
    .width('100%')
    .backgroundColor(Color.White)
    .alignItems(HorizontalAlign.Center);
  }


  @Builder
  private builderVoteOpinionView(itemBean: TestItem, index: number) {
    Row() {
      TextInput({
        text: itemBean.content,
        placeholder: `选项${index + 1}`
      })
        .layoutWeight(1)
        .maxLength(14)
        .onChange((value: string) => {
          itemBean.content = value;
        })
        .defaultFocus(false); // 弹出软键盘
      Text('删除')
        .visibility(index >= 2 ? Visibility.Visible : Visibility.None)
        .onClick(() => {
          this.testArray = this.changeArray(this.testArray, index);
        });
    };
  }
}

问题现象如下:

背景知识

ForEach:在ForEach循环渲染过程中,系统会为每个数组元素生成一个唯一且持久的键值,用于标识对应的组件。当键值变化时,ArkUI框架会视为该数组元素已被替换或修改,并会基于新的键值创建一个新的组件。其使用方式及注意事项,详见官方指南:键值生成规则与组件创建规则。

问题定位

  1. 从问题现象中可以发现在点击删除时,“选项6”为非首次渲染。从“选项3”开始依次删除时,“选项6”作为最后一个被修改的数据会一直显示到最后,直到数组的数量不足以显示“选项6”时,“选项6”消失。
  2. 从数据结构上看,ForEach渲染的每一个对象都是一致的,按照ForEach的默认keyGenerator创建规则:如果函数缺省,框架默认的键值生成函数为(item: T, index: number) => { return index + '__' + JSON.stringify(item); },也就是所有的ListItem的keyGenerator除了Index都是一致的。

对ListItem的前三次删除后,所对应的“ListItem对应的keyGenerator,ListItem显示的值”分析如下表:

ListItem

删除操作前

第一次删除后

第二次删除后

第三次删除后

选项1

0__{"id":0,"content":""},1

0__{"id":0,"content":"1"},1

0__{"id":0,"content":"1"},1

0__{"id":0,"content":"1"},1

选项2

1__{"id":0,"content":""},2

1__{"id":0,"content":"2"},2

1__{"id":0,"content":"2"},2

1__{"id":0,"content":"2"},2

选项3(每次点击删除的项)

2__{"id":0,"content":""},3

2__{"id":0,"content":"4"},4

2__{"id":0,"content":"5"},5

2__{"id":0,"content":"6"},6

选项4

3__{"id":0,"content":""},4

3__{"id":0,"content":"5"},5

3__{"id":0,"content":"6"},6

3__{"id":0,"content":""},空

选项5

4__{"id":0,"content":""},5

4__{"id":0,"content":"6"},6

4__{"id":0,"content":""},空

4__{"id":0,"content":""},空

选项6

5__{"id":0,"content":""},6

5__{"id":0,"content":""},6

5__{"id":0,"content":""},6

5__{"id":0,"content":""},6

选项7

6__{"id":0,"content":""},空

6__{"id":0,"content":""},空

6__{"id":0,"content":""},空

/

选项8

7__{"id":0,"content":""},空

7__{"id":0,"content":""},空

/

/

选项9

8__{"id":0,"content":""},空

/

/

/

分析现象总结如下:

  1. 选项6:keyGenerator一直没有变化,复用的原组件,所以输入为“6”以后,就一直显示“6”,直到对象数组长度不够,然后消失。
  2. 选项5:第一次删除后,新建为显示“6”的ListItem;第二次删除后,新建为显示“空”的ListItem,之后就一直显示“空”,直到对象数组长度不够,然后消失。
  3. 选项4:第一次删除后,新建为显示“5”的ListItem;第二次删除后,新建为显示“6”的ListItem;第三次删除后,新建为显示“空”的ListItem,之后就一直显示“空”,直到对象数组长度不够,然后消失。
  4. 选项3:第一次删除后,新建为显示“4”的ListItem;第二次删除后,新建为显示“5”的ListItem;第三次删除后,新建为显示“6”的ListItem;如果有第四次删除,第四次删除后,会新建为显示“空”的ListItem,之后就一直显示“空”,直到对象数组长度不够,然后消失。

分析结论

ForEach渲染对象数组,删除数据时,刷新异常的原因是:ForEach一开始创建的ListItem都是用的相同的对象,使得删除操作后,系统默认生成的keyGenerator无法有效区分各个ListItem是否需要新建组件,从而产生刷新异常。

修改建议

根据以上分析结论,总结方案如下:

总结

方案一与方案二都可以实现正常且符合预期的刷新操作。注意事项参考如下:

方案

优点

缺点

方案一

该方案只会对删除的选项后的ListItem进行刷新,但是对于删除的选项前面的ListItem不会再次创建,比如方案一中的“选项1”、“选项2”,由于不需要重新创建,所以会一直复用之前的ListItem。

需要注意传入的数组内的对象id不能与原数组内的对象id重复。

方案二

无需对数组内的对象做差异化处理也可对ForEach进行刷新。

由于当前点击的时间和上一次点击的时间是不一致的,导致点击前后的所有keyGenerator都会发生变化,所以会对ForEach内所有的ListItem都会重新创建,特别是当ListItem数量较多时,会产生不必要的内存消耗。