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框架会视为该数组元素已被替换或修改,并会基于新的键值创建一个新的组件。其使用方式及注意事项,详见官方指南:键值生成规则与组件创建规则。
对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":""},空 | / | / | / |
分析现象总结如下:
ForEach渲染对象数组,删除数据时,刷新异常的原因是:ForEach一开始创建的ListItem都是用的相同的对象,使得删除操作后,系统默认生成的keyGenerator无法有效区分各个ListItem是否需要新建组件,从而产生刷新异常。
根据以上分析结论,总结方案如下:
class TestItemOne {
id: number = 0;
content: string = '';
constructor(id: number) {
// 构造时传入不同的id
this.id = id;
}
}
@Entry
@Component
struct OptionOne {
@State testArray: Array<TestItemOne> = [];
// 删除数组项
changeArray(testArray: Array<TestItemOne>, index: number): Array<TestItemOne> {
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: TestItemOne, 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(() => {
// 构造时传入不同的id,此处以数组的长度作为数组最后一项的id,从而保证每个对象的id都不一致,从而刷新ForEach
this.testArray.push(new TestItemOne(this.testArray.length));
});
}
.width('100%')
.backgroundColor(Color.White)
.alignItems(HorizontalAlign.Center);
}
@Builder
private builderVoteOpinionView(itemBean: TestItemOne, index: number) {
Row({ space: 10 }) {
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);
});
};
}
} 方案一对应的“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":1,"content":""},2 | 1__{"id":1,"content":"2"},2 | 1__{"id":1,"content":"2"},2 | 1__{"id":1,"content":"2"},2 |
选项3(每次点击删除的项) | 2__{"id":2,"content":""},3 | 2__{"id":3,"content":"4"},4 | 2__{"id":4,"content":"5"},5 | 2__{"id":5,"content":"6"},6 |
选项4 | 3__{"id":3,"content":""},4 | 3__{"id":4,"content":"5"},5 | 3__{"id":5,"content":"6"},6 | 3__{"id":6,"content":""},空 |
选项5 | 4__{"id":4,"content":""},5 | 4__{"id":5,"content":"6"},6 | 4__{"id":6,"content":""},空 | 4__{"id":7,"content":""},空 |
选项6 | 5__{"id":5,"content":""},6 | 5__{"id":6,"content":""},空 | 5__{"id":7,"content":""},空 | 5__{"id":8,"content":""},空 |
选项7 | 6__{"id":6,"content":""},空 | 6__{"id":7,"content":""},空 | 6__{"id":8,"content":""},空 | / |
选项8 | 7__{"id":7,"content":""},空 | 7__{"id":8,"content":""},空 | / | / |
选项9 | 8__{"id":8,"content":""},空 | / | / | / |
从分析表可以看到每次点击“选项3”,进行删除后,由于每个数组对象的id不一致,所以从“选项3”往后的每个ListItem的keyGenerator前后都不一致,所以都会重新创建,而不是复用原ListItem组件。
class TestItemTwo {
id: number = 0;
content: string = '';
}
@Entry
@Component
struct OptionTwo {
@State testArray: Array<TestItemTwo> = [];
// 删除数组项
changeArray(testArray: Array<TestItemTwo>, index: number): Array<TestItemTwo> {
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: TestItemTwo, index: number) => {
ListItem() {
this.builderVoteOpinionView(itemBean, index);
};
}, (item: TestItemTwo, index: number) => {
return index + '__' + JSON.stringify(item) + JSON.stringify(Date.now()); // 为系统默认的返回值增加Date.now()
});
}
.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 TestItemTwo());
});
}
.width('100%')
.backgroundColor(Color.White)
.alignItems(HorizontalAlign.Center);
}
@Builder
private builderVoteOpinionView(itemBean: TestItemTwo, index: number) {
Row({ space: 10 }) {
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);
});
};
}
} 实现效果如下:

方案一与方案二都可以实现正常且符合预期的刷新操作。注意事项参考如下:
方案 | 优点 | 缺点 |
|---|---|---|
方案一 | 该方案只会对删除的选项后的ListItem进行刷新,但是对于删除的选项前面的ListItem不会再次创建,比如方案一中的“选项1”、“选项2”,由于不需要重新创建,所以会一直复用之前的ListItem。 | 需要注意传入的数组内的对象id不能与原数组内的对象id重复。 |
方案二 | 无需对数组内的对象做差异化处理也可对ForEach进行刷新。 | 由于当前点击的时间和上一次点击的时间是不一致的,导致点击前后的所有keyGenerator都会发生变化,所以会对ForEach内所有的ListItem都会重新创建,特别是当ListItem数量较多时,会产生不必要的内存消耗。 |