智能客服
你问我答,随时在线为你解决问题
ForEach接口基于数组循环渲染,需要与容器组件配合使用,且接口返回的组件应当是允许包含在ForEach父容器组件中的子组件。例如,ListItem组件要求ForEach的父容器组件必须为List组件。
API参数说明见:ForEach API参数说明。
从API version 9开始,该接口支持在ArkTS卡片中使用。
在ForEach循环渲染过程中,系统会为每个数组元素生成一个唯一且持久的键值,用于标识对应的组件。当键值变化时,ArkUI框架会视为该数组元素已被替换或修改,并会基于新的键值创建一个新的组件。
ForEach提供了一个名为keyGenerator的参数,这是一个函数,开发者可以通过它自定义键值的生成规则。如果开发者没有定义keyGenerator函数,则ArkUI框架会使用默认的键值生成函数,即(item: Object, index: number) => { return index + '__' + JSON.stringify(item); }。
ArkUI框架对于ForEach的键值生成有一套特定的判断规则,这主要与itemGenerator函数和keyGenerator函数的第二个参数index有关。具体的键值生成规则判断逻辑如下图所示。
图1 ForEach键值生成规则

键值生成示例:
- interface ChildItemType {
- str: string;
- num: number;
- }
-
- @Entry
- @Component
- struct Index {
- @State simpleList: Array<ChildItemType> = [
- { str: 'one', num: 1 },
- { str: 'two', num: 2 },
- { str: 'three', num: 3 }
- ];
-
- build() {
- Row() {
- Column() {
- ForEach(this.simpleList, (item: ChildItemType, index: number) => {
- ChildItem({ str: item.str, num: index }) // 组件生成函数中使用index参数
- }, (item: ChildItemType, index: number) => {
- return item.str; // 建议在键值生成函数中使用与UI界面相关的数据属性str
- })
- }
- .width('100%')
- .height('100%')
- }
- .height('100%')
- .backgroundColor(0xF1F3F5)
- }
- }
-
- @Component
- struct ChildItem {
- @Prop str: string = '';
- @Prop num: number = 0;
-
- build() {
- Text(this.str)
- .fontSize(50)
- }
- }
在上述示例中,当组件生成函数声明index时,建议键值生成函数也声明index参数,以避免渲染性能降低和渲染结果非预期。同时建议在键值生成函数实现中使用与UI相关的数据属性,在本示例中,数据属性str与UI界面显示相关,因此建议将其作为键值生成函数的返回值。
在确定键值生成规则后,ForEach的第二个参数itemGenerator函数会根据键值生成规则为数据源的每个数组项创建组件。组件的创建包括两种情况:ForEach首次渲染和ForEach非首次渲染。
在ForEach首次渲染时,会根据前述键值生成规则为数据源的每个数组项生成唯一键值,并创建相应的组件。
- @Entry
- @Component
- struct ForEachFirstRender {
- @State simpleList: Array<string> = ['one', 'two', 'three'];
-
- build() {
- Row() {
- Column() {
- ForEach(this.simpleList, (item: string) => {
- ForEachChildItem({ item: item })
- }, (item: string) => item) // 需要保证key唯一
- }
- .width('100%')
- .height('100%')
- }
- .height('100%')
- .backgroundColor(0xF1F3F5)
- }
- }
-
- @Component
- struct ForEachChildItem {
- @Prop item: string;
-
- build() {
- Text(this.item)
- .fontSize(50)
- }
- }
运行效果如下图所示。
图2 ForEach数据项不存在相同键值案例首次渲染运行效果图

在上述代码中,keyGenerator函数的返回值是item。在ForEach渲染循环时,为数组项依次生成键值one、two和three,并创建对应的ForEachChildItem组件渲染到界面上。
当不同数组项生成的键值相同时,框架的行为是未定义的。例如,在以下代码中,ForEach渲染相同的数据项two时,只创建了一个SameKeyChildItem组件,而没有创建多个具有相同键值的组件。
- @Entry
- @Component
- struct ForEachSameKey {
- @State simpleList: Array<string> = ['one', 'two', 'two', 'three'];
-
- build() {
- Row() {
- Column() {
- ForEach(this.simpleList, (item: string) => {
- SameKeyChildItem({ item: item })
- }, (item: string) => item)
- }
- .width('100%')
- .height('100%')
- }
- .height('100%')
- .backgroundColor(0xF1F3F5)
- }
- }
-
- @Component
- struct SameKeyChildItem {
- @Prop item: string;
-
- build() {
- Text(this.item)
- .fontSize(50)
- }
- }
运行效果如下图所示。
图3 ForEach数据源存在相同值案例首次渲染运行效果图

在该示例中,最终键值生成规则为item。当ForEach遍历数据源simpleList,遍历到索引为1的two时,创建键值为two的组件并记录。当遍历到索引为2的two时,当前项的键值也为two,此时不再创建新的组件。
在ForEach组件进行非首次渲染时,它会检查新生成的键值是否在上次渲染中已经存在。如果键值不存在,则会创建一个新的组件;如果键值存在,则不会创建新的组件,而是直接渲染该键值所对应的组件。例如,在以下的代码示例中,通过点击事件修改了数组的第三项值为"new three",这将触发ForEach组件进行非首次渲染。
- @Entry
- @Component
- struct ForEachNotFirstRender {
- @State simpleList: Array<string> = ['one', 'two', 'three'];
-
- build() {
- Row() {
- Column() {
- Text('Click to change the value of the third array item')
- .fontSize(24)
- .fontColor(Color.Red)
- .onClick(() => {
- this.simpleList[2] = 'new three';
- })
-
- ForEach(this.simpleList, (item: string) => {
- NotFirstRenderChildItem({ item: item })
- .margin({ top: 20 })
- }, (item: string) => item)
- }
- .justifyContent(FlexAlign.Center)
- .width('100%')
- .height('100%')
- }
- .height('100%')
- .backgroundColor(0xF1F3F5)
- }
- }
-
- @Component
- struct NotFirstRenderChildItem {
- @Prop item: string;
-
- build() {
- Text(this.item)
- .fontSize(30)
- }
- }
运行效果如下图所示。
图4 ForEach非首次渲染案例运行效果图

从本例可以看出@State能够监听到简单数据类型数组simpleList数组项的变化。
ForEach组件在开发过程中的主要应用场景包括:数据源不变、数据源数组项发生变化(如插入、删除操作)、数据源数组项子属性变化。
在数据源保持不变的场景中,数据源可以直接采用基本数据类型。例如,页面加载状态时,可以使用骨架屏列表进行渲染展示。
- @Entry
- @Component
- struct ArticleList {
- @State simpleList: Array<number> = [1, 2, 3, 4, 5];
-
- build() {
- Column() {
- ForEach(this.simpleList, (item: number) => {
- ArticleSkeletonView()
- .margin({ top: 20 })
- }, (item: number) => item.toString())
- }
- .padding(20)
- .width('100%')
- .height('100%')
- }
- }
-
- @Builder
- function textArea(width: number | Resource | string = '100%', height: number | Resource | string = '100%') {
- Row()
- .width(width)
- .height(height)
- .backgroundColor('#FFF2F3F4')
- }
-
- @Component
- struct ArticleSkeletonView {
- build() {
- Row() {
- Column() {
- textArea(80, 80)
- }
- .margin({ right: 20 })
-
- Column() {
- textArea('60%', 20)
- textArea('50%', 20)
- }
- .alignItems(HorizontalAlign.Start)
- .justifyContent(FlexAlign.SpaceAround)
- .height('100%')
- }
- .padding(20)
- .borderRadius(12)
- .backgroundColor('#FFECECEC')
- .height(120)
- .width('100%')
- .justifyContent(FlexAlign.SpaceBetween)
- }
- }
运行效果如下图所示。
图5 骨架屏运行效果图

在本示例中,采用数据项item作为键值生成规则,由于数据源simpleList的数组项各不相同,因此能够保证键值的唯一性。
在数据源数组项发生变化的场景下,如数组插入、删除操作或者数组项索引位置交换时,数据源应为对象数组类型,并使用对象的唯一ID作为键值。
- class ArticleChangeSource {
- public id: string;
- public title: string;
- public brief: string;
-
- constructor(id: string, title: string, brief: string) {
- this.id = id;
- this.title = title;
- this.brief = brief;
- }
- }
-
- @Entry
- @Component
- struct ArticleListViewChangeSource {
- isListReachEnd: boolean = false;
- @State articleList: Array<ArticleChangeSource> = [
- new ArticleChangeSource('001', 'Article 1', 'Abstract'),
- new ArticleChangeSource('002', 'Article 2', 'Abstract'),
- new ArticleChangeSource('003', 'Article 3', 'Abstract'),
- new ArticleChangeSource('004', 'Article 4', 'Abstract'),
- new ArticleChangeSource('005', 'Article 5', 'Abstract'),
- new ArticleChangeSource('006', 'Article 6', 'Abstract')
- ];
-
- loadMoreArticles() {
- this.articleList.push(new ArticleChangeSource('007', 'New Article', 'Abstract'));
- }
-
- build() {
- Column({ space: 5 }) {
- List() {
- ForEach(this.articleList, (item: ArticleChangeSource) => {
- ListItem() {
- ArticleCardChangeSource({ article: item })
- .margin({ top: 20 })
- }
- }, (item: ArticleChangeSource) => item.id)
- }
- .onReachEnd(() => {
- this.isListReachEnd = true;
- })
- .parallelGesture(
- PanGesture({ direction: PanDirection.Up, distance: 80 })
- .onActionStart(() => {
- if (this.isListReachEnd) {
- this.loadMoreArticles();
- this.isListReachEnd = false;
- }
- })
- )
- .padding(20)
- .scrollBar(BarState.Off)
- }
- .width('100%')
- .height('100%')
- .backgroundColor(0xF1F3F5)
- }
- }
-
- @Component
- struct ArticleCardChangeSource {
- @Prop article: ArticleChangeSource;
-
- build() {
- Row() {
- // 此处'app.media.startIcon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
- Image($r('app.media.startIcon'))
- .width(80)
- .height(80)
- .margin({ right: 20 })
-
- Column() {
- Text(this.article.title)
- .fontSize(20)
- .margin({ bottom: 8 })
- Text(this.article.brief)
- .fontSize(16)
- .fontColor(Color.Gray)
- .margin({ bottom: 8 })
- }
- .alignItems(HorizontalAlign.Start)
- .width('80%')
- .height('100%')
- }
- .padding(20)
- .borderRadius(12)
- .backgroundColor('#FFECECEC')
- .height(120)
- .width('100%')
- .justifyContent(FlexAlign.SpaceBetween)
- }
- }
初始运行效果(左图)和手势上滑加载后效果(右图)如下图所示。
图6 数据源数组项变化案例运行效果图

在本示例中,ArticleCardChangeSource组件作为ArticleListViewChangeSource组件的子组件,通过@Prop装饰器接收一个ArticleChangeSource对象,用于渲染文章卡片。
当数据源的数组项为对象数据类型,并且只修改某个数组项的属性值时,由于数据源为复杂数据类型,ArkUI框架无法监听到@State装饰器修饰的数据源数组项的属性变化,从而无法触发ForEach的重新渲染。为实现ForEach子组件重新渲染,需要结合@Observed和@ObjectLink装饰器使用。例如,在文章列表卡片上点击“点赞”按钮,从而修改文章的点赞数量。
- @Observed
- class ArticleChangeChild {
- public id: string;
- public title: string;
- public brief: string;
- public isLiked: boolean;
- public likesCount: number;
-
- constructor(id: string, title: string, brief: string, isLiked: boolean, likesCount: number) {
- this.id = id;
- this.title = title;
- this.brief = brief;
- this.isLiked = isLiked;
- this.likesCount = likesCount;
- }
- }
-
- @Entry
- @Component
- struct ArticleListChangeView {
- @State articleList: Array<ArticleChangeChild> = [
- new ArticleChangeChild('001', 'Article 0', 'Abstract', false, 100),
- new ArticleChangeChild('002', 'Article 1', 'Abstract', false, 100),
- new ArticleChangeChild('003', 'Article 2', 'Abstract', false, 100),
- new ArticleChangeChild('004', 'Article 4', 'Abstract', false, 100),
- new ArticleChangeChild('005', 'Article 5', 'Abstract', false, 100),
- new ArticleChangeChild('006', 'Article 6', 'Abstract', false, 100),
- ];
-
- build() {
- List() {
- ForEach(this.articleList, (item: ArticleChangeChild) => {
- ListItem() {
- ArticleCardChangeChild({
- article: item
- })
- .margin({ top: 20 })
- }
- }, (item: ArticleChangeChild) => item.id)
- }
- .padding(20)
- .scrollBar(BarState.Off)
- .backgroundColor(0xF1F3F5)
- }
- }
-
- @Component
- struct ArticleCardChangeChild {
- @ObjectLink article: ArticleChangeChild;
-
- handleLiked() {
- this.article.isLiked = !this.article.isLiked;
- this.article.likesCount = this.article.isLiked ? this.article.likesCount + 1 : this.article.likesCount - 1;
- }
-
- build() {
- Row() {
- // 此处'app.media.startIcon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
- Image($r('app.media.startIcon'))
- .width(80)
- .height(80)
- .margin({ right: 20 })
-
- Column() {
- Text(this.article.title)
- .fontSize(20)
- .margin({ bottom: 8 })
- Text(this.article.brief)
- .fontSize(16)
- .fontColor(Color.Gray)
- .margin({ bottom: 8 })
-
- Row() {
- // 此处'app.media.iconLiked','app.media.iconUnLiked'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
- Image(this.article.isLiked ? $r('app.media.iconLiked') : $r('app.media.iconUnLiked'))
- .width(24)
- .height(24)
- .margin({ right: 8 })
- Text(this.article.likesCount.toString())
- .fontSize(16)
- }
- .onClick(() => this.handleLiked())
- .justifyContent(FlexAlign.Center)
- }
- .alignItems(HorizontalAlign.Start)
- .width('80%')
- .height('100%')
- }
- .padding(20)
- .borderRadius(12)
- .backgroundColor('#FFECECEC')
- .height(120)
- .width('100%')
- .justifyContent(FlexAlign.SpaceBetween)
- }
- }
上述代码的初始运行效果(左图)和点击第1个文章卡片上的点赞图标后的运行效果(右图)如下图所示。
图7 数据源数组项子属性变化案例运行效果图

在本示例中,ArticleChangeChild类被@Observed装饰器修饰。父组件ArticleListChangeView传入ArticleChangeChild对象实例给子组件ArticleCardChangeChild,子组件使用@ObjectLink装饰器接收该实例。
在List组件下使用ForEach,并设置onMove事件,每次迭代生成一个ListItem时,可以使能拖拽排序。拖拽排序离手后,如果组件位置发生变化,将触发onMove事件,上报组件移动原始索引号和目标索引号。在onMove事件中,需要根据上报的起始索引号和目标索引号修改数据源。数据源修改前后,要保持每个数据的键值不变,只是顺序发生变化,才能保证落位动画正常执行。
- @Entry
- @Component
- struct ForEachSort {
- @State arr: Array<string> = [];
-
- build() {
- Column() {
- // 点击此按钮会触发ForEach重新渲染
- Button('Add one item')
- .onClick(() => {
- this.arr.push('10');
- })
- .width(300)
- .margin(10)
-
- List() {
- ForEach(this.arr, (item: string) => {
- ListItem() {
- Text(item.toString())
- .fontSize(16)
- .textAlign(TextAlign.Center)
- .size({ height: 100, width: '100%' })
- }.margin(10)
- .borderRadius(10)
- .backgroundColor('#FFFFFFFF')
- }, (item: string) => item)
- .onMove((from: number, to: number) => {
- // 以下两行代码是为了确保拖拽后屏幕上组件的顺序与数组arr中每一项的顺序保持一致。
- // 若注释以下两行,第一步拖拽排序,第二步在arr末尾插入一项,触发ForEach渲染,此时屏上组件的顺序会跟数组arr中每一项的顺序一致,而不是维持第一步拖拽后的顺序,意味着拖拽排序在ForEach渲染后失效了。
- let tmp = this.arr.splice(from, 1);
- this.arr.splice(to, 0, tmp[0]);
- })
- }
- .width('100%')
- .height('100%')
- .backgroundColor('#FFDCDCDC')
- }
- }
-
- aboutToAppear(): void {
- for (let i = 0; i < 10; i++) {
- this.arr.push(i.toString());
- }
- }
- }
图8 ForEach拖拽排序效果图

注释掉onMove事件调用中的两行代码,点击Add one item触发渲染后的效果如下图所示。
图9 ForEach拖拽排序效果在重新渲染后没有保留

在本示例中,通过设置ForEach的第三个参数KeyGenerator函数,自定义键值生成规则为数据源的索引index的字符串类型值。当点击父组件ForEachAbnormal中“Insert Item After First Item”文本组件后,界面会出现非预期的结果。
- @Entry
- @Component
- struct ForEachAbnormal {
- @State simpleList: Array<string> = ['one', 'two', 'three'];
-
- build() {
- Column() {
- Button() {
- Text('Insert Item After First Item').fontSize(30)
- }
- .onClick(() => {
- this.simpleList.splice(1, 0, 'new item');
- })
-
- ForEach(this.simpleList, (item: string) => {
- ForEachAbnormalChildItem({ item: item })
- }, (item: string, index: number) => index.toString())
- }
- .justifyContent(FlexAlign.Center)
- .width('100%')
- .height('100%')
- .backgroundColor(0xF1F3F5)
- }
- }
-
- @Component
- struct ForEachAbnormalChildItem {
- @Prop item: string;
-
- build() {
- Text(this.item)
- .fontSize(30)
- }
- }
上述代码的初始渲染效果和点击“在第1项后插入新项”文本组件后的渲染效果如下图所示。
图10 渲染结果非预期运行效果图

ForEach在首次渲染时,创建的键值依次为"0"、"1"、"2"。
插入新项后,数据源simpleList变为['one', 'new item', 'two', 'three'],框架监听到@State装饰的数据源长度变化触发ForEach重新渲染。
ForEach依次遍历新数据源,遍历数据项"one"时生成键值"0",存在相同键值,因此不创建新组件。继续遍历数据项"new item"时生成键值"1",存在相同键值,因此不创建新组件。继续遍历数据项"two"生成键值"2",存在相同键值,因此不创建新组件。最后遍历数据项"three"时生成键值"3",不存在相同键值,创建内容为"three"的新组件并渲染。
从以上可以看出,当键值包含数据项索引index时,期望的界面渲染结果为['one', 'new item', 'two', 'three'],而实际的渲染结果为['one', 'two', 'three', 'three'],不符合开发者预期。因此,开发者在使用ForEach时应避免键值包含索引index。
在本示例中,ForEach的第三个参数KeyGenerator函数缺省。根据上述键值生成规则,此例使用框架默认的键值,即最终键值为字符串index + '__' + JSON.stringify(item)。点击文本组件“在第1项后插入新项”后,ForEach将为第2个数组项及后面的所有数据项重新创建组件。
- import { hilog } from '@kit.PerformanceAnalysisKit';
- const TAG = '[Sample_RenderingControl]';
- const DOMAIN = 0xF811;
-
- @Entry
- @Component
- struct ReducedRenderingPerformance {
- @State simpleList: Array<string> = ['one', 'two', 'three'];
-
- build() {
- Column() {
- Button() {
- Text('Insert Item After First Item').fontSize(30)
- }
- .onClick(() => {
- this.simpleList.splice(1, 0, 'new item');
- hilog.info(DOMAIN, 'testTag', `[onClick]: simpleList is [${this.simpleList.join(', ')}]`);
- })
-
- ForEach(this.simpleList, (item: string) => {
- ReducedChildItem({ item: item })
- })
- }
- .justifyContent(FlexAlign.Center)
- .width('100%')
- .height('100%')
- .backgroundColor(0xF1F3F5)
- }
- }
-
- @Component
- struct ReducedChildItem {
- @Prop item: string;
-
- aboutToAppear() {
- hilog.info(DOMAIN, TAG, `[aboutToAppear]: item is ${this.item}`);
- }
-
- build() {
- Text(this.item)
- .fontSize(50)
- }
- }
以上代码的初始渲染效果和点击"Insert Item After First Item"文本组件后的渲染效果如下图所示。
图11 渲染性能降低案例运行效果图

点击“Insert Item After First Item”文本组件后,DevEco Studio的日志打印结果如下所示。
图12 渲染性能降低案例日志打印图

插入新项后,ForEach为new item、 two、 three三个数组项创建了对应的ReducedChildItem组件,并执行了组件的aboutToAppear()生命周期函数。这是因为:
尽管本例中界面渲染结果符合预期,但在每次向数组中间插入新数组项时,ForEach会为该数组项及其后面的所有数组项重新创建组件。当数据源数据量较大或组件结构复杂时,组件无法复用会导致性能下降。因此,不建议省略第三个参数KeyGenerator函数,也不建议在键值中使用数据项索引index。
正确渲染并保证效率的ForEach写法是:
- ForEach(this.simpleList, (item: string) => {
- ForEachChildItem({ item: item })
- }, (item: string) => item) // 需要保证key唯一
提供了第三个参数KeyGenerator,在这个例子中,对数据源的不同数据项生成不同的key,并且对同一个数据项每次生成相同的key。
点击按钮Like/UnLike first article,第一个组件会切换点赞手势和后面的点赞数量,但是点击按钮Replace first article之后再点击按钮Like/UnLike first article就不生效了。原因是替换articleList[0]之后,articleList状态变量发生变化,触发ForEach重新渲染,但是新的articleList[0]生成的key没有变,ForEach不会将数据更新同步给子组件,因此第一个组件仍然绑定旧的articleList[0]。新articleList[0]的属性发生变更,第一个组件感知不到,不会重新渲染。点击点赞手势,会触发渲染。因为变更的是跟组件绑定的数组项的属性,组件会感知并重新渲染。
- @Observed
- class ArticleChangeData {
- public id: string;
- public title: string;
- public brief: string;
- public isLiked: boolean;
- public likesCount: number;
-
- constructor(id: string, title: string, brief: string, isLiked: boolean, likesCount: number) {
- this.id = id;
- this.title = title;
- this.brief = brief;
- this.isLiked = isLiked;
- this.likesCount = likesCount;
- }
- }
-
- @Entry
- @Component
- struct ArticleListChangeData {
- @State articleList: Array<ArticleChangeData> = [
- new ArticleChangeData('001', 'Article 0', 'Abstract', false, 100),
- new ArticleChangeData('002', 'Article 1', 'Abstract', false, 100),
- new ArticleChangeData('003', 'Article 2', 'Abstract', false, 100),
- new ArticleChangeData('004', 'Article 4', 'Abstract', false, 100),
- new ArticleChangeData('005', 'Article 5', 'Abstract', false, 100),
- new ArticleChangeData('006', 'Article 6', 'Abstract', false, 100),
- ];
-
- build() {
- Column() {
- Button('Replace first article')
- .onClick(() => {
- this.articleList[0] = new ArticleChangeData('001', 'Article 0', 'Abstract', false, 100);
- })
- .width(300)
- .margin(10)
-
- Button('Like/Unlike first article')
- .onClick(() => {
- this.articleList[0].isLiked = !this.articleList[0].isLiked;
- this.articleList[0].likesCount =
- this.articleList[0].isLiked ? this.articleList[0].likesCount + 1 : this.articleList[0].likesCount - 1;
- })
- .width(300)
- .margin(10)
-
- List() {
- ForEach(this.articleList, (item: ArticleChangeData) => {
- ListItem() {
- ArticleCardChangeData({
- article: item
- })
- .margin({ top: 20 })
- }
- }, (item: ArticleChangeData) => item.id)
- }
- .padding(20)
- .scrollBar(BarState.Off)
- .backgroundColor(0xF1F3F5)
- }
- }
- }
-
- @Component
- struct ArticleCardChangeData {
- @ObjectLink article: ArticleChangeData;
-
- handleLiked() {
- this.article.isLiked = !this.article.isLiked;
- this.article.likesCount = this.article.isLiked ? this.article.likesCount + 1 : this.article.likesCount - 1;
- }
-
- build() {
- Row() {
- // 此处'app.media.startIcon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
- Image($r('app.media.startIcon'))
- .width(80)
- .height(80)
- .margin({ right: 20 })
-
- Column() {
- Text(this.article.title)
- .fontSize(20)
- .margin({ bottom: 8 })
- Text(this.article.brief)
- .fontSize(16)
- .fontColor(Color.Gray)
- .margin({ bottom: 8 })
-
- Row() {
- // 此处'app.media.iconLiked','app.media.iconUnLiked'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
- Image(this.article.isLiked ? $r('app.media.iconLiked') : $r('app.media.iconUnLiked'))
- .width(24)
- .height(24)
- .margin({ right: 8 })
- Text(this.article.likesCount.toString())
- .fontSize(16)
- }
- .onClick(() => this.handleLiked())
- .justifyContent(FlexAlign.Center)
- }
- .alignItems(HorizontalAlign.Start)
- .width('80%')
- .height('100%')
- }
- .padding(20)
- .borderRadius(12)
- .backgroundColor('#FFECECEC')
- .height(120)
- .width('100%')
- .justifyContent(FlexAlign.SpaceBetween)
- }
- }
图13 数据变化不渲染

如果开发者没有定义keyGenerator函数,则ArkUI框架会使用默认的键值生成函数,即(item: Object, index: number) => { return index + '__' + JSON.stringify(item); }。当item是复杂对象时,将其JSON序列化会得到长字符串,占用更多的内存。
- class MemoryData {
- public longStr: string;
- public key: string;
-
- constructor(longStr: string, key: string) {
- this.longStr = longStr;
- this.key = key;
- }
- }
-
- @Entry
- @Component
- struct NonNecessaryMemory {
- @State simpleList: Array<MemoryData> = [];
-
- aboutToAppear(): void {
- let longStr = '';
- for (let i = 0; i < 2000; i++) {
- longStr += i.toString();
- }
- for (let index = 0; index < 3000; index++) {
- let data: MemoryData = new MemoryData(longStr, 'a' + index.toString());
- this.simpleList.push(data);
- }
- }
-
- build() {
- List() {
- ForEach(this.simpleList, (item: MemoryData) => {
- ListItem() {
- Text(item.key)
- }
- }
- // 如果不定义下面的keyGenerator函数,则ArkUI框架会使用默认的键值生成函数
- , (item: MemoryData) => {
- return item.key;
- }
- )
- }.height('100%')
- .width('100%')
- }
- }
对比自定义keyGenerator函数和使用默认键值生成函数两种情况下的内存占用(通过DevEco->Profiler->Realtime Monitor工具,可以获取相关进程的内存数据)。自定义keyGenerator函数,这个示例代码的内存占用降低了约70MB。
图14 使用默认键值生成函数下的内存占用

图15 自定义键值生成函数下的内存占用

如果开发者没有定义keyGenerator函数,则ArkUI框架会使用默认的键值生成函数,即(item: Object, index: number) => { return index + '__' + JSON.stringify(item); }。然而,JSON.stringify序列化在某些数据结构上会失败,导致应用发生jscrash并退出。例如,bigint无法被JSON.stringify序列化:
- class KeyData {
- public content: bigint;
-
- constructor(content: bigint) {
- this.content = content;
- }
- }
-
- @Entry
- @Component
- struct GenerationKeyExample {
- @State simpleList: Array<KeyData> = [new KeyData(1234567890123456789n), new KeyData(2345678910987654321n)];
-
- build() {
- Row() {
- Column() {
- ForEach(this.simpleList, (item: KeyData) => {
- GenerationKeyChildItem({ item: item.content.toString() })
- }
- // 如果不定义下面的keyGenerator函数,则ArkUI框架会使用默认的键值生成函数
- // KeyData中的content: bigint在JSON序列化时失败
- , (item: KeyData) => item.content.toString()
- )
- }
- .width('100%')
- .height('100%')
- }
- .height('100%')
- .backgroundColor(0xF1F3F5)
- }
- }
-
- @Component
- struct GenerationKeyChildItem {
- @Prop item: string;
-
- build() {
- Text(this.item)
- .fontSize(50)
- }
- }
开发者定义keyGenerator函数,应用正常启动:

使用默认的键值生成函数,应用发生jscrash:
- Error message:@Component 'Parent'[4]: ForEach id 7: use of default id generator function not possible on provided data structure. Need to specify id generator function (ForEach 3rd parameter). Application Error!
- Stacktrace:
- ...
- at anonymous (entry/src/main/ets/pages/Index.ets:18:52)