ForEach接口基于數(shù)組類型數(shù)據(jù)來進(jìn)行循環(huán)渲染,需要與容器組件配合使用,且接口返回的組件應(yīng)當(dāng)是允許包含在ForEach父容器組件中的子組件。例如,ListItem組件要求ForEach的父容器組件必須為List組件。
從API version 9開始,該接口支持在ArkTS卡片中使用。
- ForEach(
- arr: Array,
- itemGenerator: (item: any, index?: number) => void,
- keyGenerator?: (item: any, index?: number) => string
- )
參數(shù)名 | 參數(shù)類型 | 必填 | 參數(shù)描述 |
---|---|---|---|
arr | Array | 是 | 數(shù)據(jù)源,為Array類型的數(shù)組。 說明: - 可以設(shè)置為空數(shù)組,此時(shí)不會(huì)創(chuàng)建子組件。 - 可以設(shè)置返回值為數(shù)組類型的函數(shù),例如arr.slice(1, 3),但設(shè)置的函數(shù)不應(yīng)改變包括數(shù)組本身在內(nèi)的任何狀態(tài)變量,例如不應(yīng)使用Array.splice(),Array.sort()或Array.reverse()這些會(huì)改變?cè)瓟?shù)組的函數(shù)。 |
itemGenerator | (item: any, index?: number) => void | 是 | 組件生成函數(shù)。 - 為數(shù)組中的每個(gè)元素創(chuàng)建對(duì)應(yīng)的組件。 - item參數(shù):arr數(shù)組中的數(shù)據(jù)項(xiàng)。 - index參數(shù)(可選):arr數(shù)組中的數(shù)據(jù)項(xiàng)索引。 說明: - 組件的類型必須是ForEach的父容器所允許的。例如,ListItem組件要求ForEach的父容器組件必須為L(zhǎng)ist組件。 |
keyGenerator | (item: any, index?: number) => string | 否 | 鍵值生成函數(shù)。 - 為數(shù)據(jù)源arr的每個(gè)數(shù)組項(xiàng)生成唯一且持久的鍵值。函數(shù)返回值為開發(fā)者自定義的鍵值生成規(guī)則。 - item參數(shù):arr數(shù)組中的數(shù)據(jù)項(xiàng)。- index參數(shù)(可選):arr數(shù)組中的數(shù)據(jù)項(xiàng)索引。 說明: - 如果函數(shù)缺省,框架默認(rèn)的鍵值生成函數(shù)為(item: T, index: number) => { return index + '__' + JSON.stringify(item); } - 鍵值生成函數(shù)不應(yīng)改變?nèi)魏谓M件狀態(tài)。 |
在ForEach循環(huán)渲染過程中,系統(tǒng)會(huì)為每個(gè)數(shù)組元素生成一個(gè)唯一且持久的鍵值,用于標(biāo)識(shí)對(duì)應(yīng)的組件。當(dāng)這個(gè)鍵值變化時(shí),ArkUI框架將視為該數(shù)組元素已被替換或修改,并會(huì)基于新的鍵值創(chuàng)建一個(gè)新的組件。
ForEach提供了一個(gè)名為keyGenerator的參數(shù),這是一個(gè)函數(shù),開發(fā)者可以通過它自定義鍵值的生成規(guī)則。如果開發(fā)者沒有定義keyGenerator函數(shù),則ArkUI框架會(huì)使用默認(rèn)的鍵值生成函數(shù),即(item: any, index: number) => { return index + '__' + JSON.stringify(item); }。
ArkUI框架對(duì)于ForEach的鍵值生成有一套特定的判斷規(guī)則,這主要與itemGenerator函數(shù)的第二個(gè)參數(shù)index以及keyGenerator函數(shù)的第二個(gè)參數(shù)index有關(guān),具體的鍵值生成規(guī)則判斷邏輯如下圖所示。
ArkUI框架會(huì)對(duì)重復(fù)的鍵值發(fā)出警告。在UI更新的場(chǎng)景下,如果出現(xiàn)重復(fù)的鍵值,框架可能無法正常工作,具體請(qǐng)參見渲染結(jié)果非預(yù)期。
在確定鍵值生成規(guī)則后,F(xiàn)orEach的第二個(gè)參數(shù)itemGenerator函數(shù)會(huì)根據(jù)鍵值生成規(guī)則為數(shù)據(jù)源的每個(gè)數(shù)組項(xiàng)創(chuàng)建組件。組件的創(chuàng)建包括兩種情況:ForEach首次渲染和ForEach非首次渲染。
在ForEach首次渲染時(shí),會(huì)根據(jù)前述鍵值生成規(guī)則為數(shù)據(jù)源的每個(gè)數(shù)組項(xiàng)生成唯一鍵值,并創(chuàng)建相應(yīng)的組件。
- @Entry
- @Component
- struct Parent {
- @State simpleList: Array<string> = ['one', 'two', 'three'];
- build() {
- Row() {
- Column() {
- ForEach(this.simpleList, (item: string) => {
- ChildItem({ 'item': item } as Record<string, string>)
- }, (item: string) => item)
- }
- .width('100%')
- .height('100%')
- }
- .height('100%')
- .backgroundColor(0xF1F3F5)
- }
- }
- @Component
- struct ChildItem {
- @Prop item: string;
- build() {
- Text(this.item)
- .fontSize(50)
- }
- }
運(yùn)行效果如下圖所示。
在上述代碼中,鍵值生成規(guī)則是keyGenerator函數(shù)的返回值item。在ForEach渲染循環(huán)時(shí),為數(shù)據(jù)源數(shù)組項(xiàng)依次生成鍵值one、two和three,并創(chuàng)建對(duì)應(yīng)的ChildItem組件渲染到界面上。
當(dāng)不同數(shù)組項(xiàng)按照鍵值生成規(guī)則生成的鍵值相同時(shí),框架的行為是未定義的。例如,在以下代碼中,F(xiàn)orEach渲染相同的數(shù)據(jù)項(xiàng)two時(shí),只創(chuàng)建了一個(gè)ChildItem組件,而沒有創(chuàng)建多個(gè)具有相同鍵值的組件。
- @Entry
- @Component
- struct Parent {
- @State simpleList: Array<string> = ['one', 'two', 'two', 'three'];
- build() {
- Row() {
- Column() {
- ForEach(this.simpleList, (item: string) => {
- ChildItem({ 'item': item } as Record<string, string>)
- }, (item: string) => item)
- }
- .width('100%')
- .height('100%')
- }
- .height('100%')
- .backgroundColor(0xF1F3F5)
- }
- }
- @Component
- struct ChildItem {
- @Prop item: string;
- build() {
- Text(this.item)
- .fontSize(50)
- }
- }
運(yùn)行效果如下圖所示。
在該示例中,最終鍵值生成規(guī)則為item。當(dāng)ForEach遍歷數(shù)據(jù)源simpleList,遍歷到索引為1的two時(shí),按照最終鍵值生成規(guī)則生成鍵值為two的組件并進(jìn)行標(biāo)記。當(dāng)遍歷到索引為2的two時(shí),按照最終鍵值生成規(guī)則當(dāng)前項(xiàng)的鍵值也為two,此時(shí)不再創(chuàng)建新的組件。
在ForEach組件進(jìn)行非首次渲染時(shí),它會(huì)檢查新生成的鍵值是否在上次渲染中已經(jīng)存在。如果鍵值不存在,則會(huì)創(chuàng)建一個(gè)新的組件;如果鍵值存在,則不會(huì)創(chuàng)建新的組件,而是直接渲染該鍵值所對(duì)應(yīng)的組件。例如,在以下的代碼示例中,通過點(diǎn)擊事件修改了數(shù)組的第三項(xiàng)值為"new three",這將觸發(fā)ForEach組件進(jìn)行非首次渲染。
- @Entry
- @Component
- struct Parent {
- @State simpleList: Array<string> = ['one', 'two', 'three'];
- build() {
- Row() {
- Column() {
- Text('點(diǎn)擊修改第3個(gè)數(shù)組項(xiàng)的值')
- .fontSize(24)
- .fontColor(Color.Red)
- .onClick(() => {
- this.simpleList[2] = 'new three';
- })
- ForEach(this.simpleList, (item: string) => {
- ChildItem({ item: item })
- .margin({ top: 20 })
- }, (item: string) => item)
- }
- .justifyContent(FlexAlign.Center)
- .width('100%')
- .height('100%')
- }
- .height('100%')
- .backgroundColor(0xF1F3F5)
- }
- }
- @Component
- struct ChildItem {
- @Prop item: string;
- build() {
- Text(this.item)
- .fontSize(30)
- }
- }
運(yùn)行效果如下圖所示。
從本例可以看出@State 能夠監(jiān)聽到簡(jiǎn)單數(shù)據(jù)類型數(shù)組數(shù)據(jù)源 simpleList 數(shù)組項(xiàng)的變化。
ForEach組件在開發(fā)過程中的主要應(yīng)用場(chǎng)景包括:數(shù)據(jù)源不變、數(shù)據(jù)源數(shù)組項(xiàng)發(fā)生變化(如插入、刪除操作)、數(shù)據(jù)源數(shù)組項(xiàng)子屬性變化。
在數(shù)據(jù)源保持不變的場(chǎng)景中,數(shù)據(jù)源可以直接采用基本數(shù)據(jù)類型。例如,在頁(yè)面加載狀態(tài)時(shí),可以使用骨架屏列表進(jìn)行渲染展示。
- @Entry
- @Component
- struct ArticleList {
- @State simpleList: Array<number> = [1, 2, 3, 4, 5];
- build() {
- Column() {
- ForEach(this.simpleList, (item: string) => {
- ArticleSkeletonView()
- .margin({ top: 20 })
- }, (item: string) => item)
- }
- .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)
- }
- }
運(yùn)行效果如下圖所示。
在本示例中,采用數(shù)據(jù)項(xiàng)item作為鍵值生成規(guī)則,由于數(shù)據(jù)源simpleList的數(shù)組項(xiàng)各不相同,因此能夠保證鍵值的唯一性。
在數(shù)據(jù)源數(shù)組項(xiàng)發(fā)生變化的場(chǎng)景下,例如進(jìn)行數(shù)組插入、刪除操作或者數(shù)組項(xiàng)索引位置發(fā)生交換時(shí),數(shù)據(jù)源應(yīng)為對(duì)象數(shù)組類型,并使用對(duì)象的唯一ID作為最終鍵值。例如,當(dāng)在頁(yè)面上通過手勢(shì)上滑加載下一頁(yè)數(shù)據(jù)時(shí),會(huì)在數(shù)據(jù)源數(shù)組尾部新增新獲取的數(shù)據(jù)項(xiàng),從而使得數(shù)據(jù)源數(shù)組長(zhǎng)度增大。
- @Entry
- @Component
- struct ArticleListView {
- @State isListReachEnd: boolean = false;
- @State articleList: Array<Article> = [
- new Article('001', '第1篇文章', '文章簡(jiǎn)介內(nèi)容'),
- new Article('002', '第2篇文章', '文章簡(jiǎn)介內(nèi)容'),
- new Article('003', '第3篇文章', '文章簡(jiǎn)介內(nèi)容'),
- new Article('004', '第4篇文章', '文章簡(jiǎn)介內(nèi)容'),
- new Article('005', '第5篇文章', '文章簡(jiǎn)介內(nèi)容'),
- new Article('006', '第6篇文章', '文章簡(jiǎn)介內(nèi)容')
- ]
- loadMoreArticles() {
- this.articleList.push(new Article('007', '加載的新文章', '文章簡(jiǎn)介內(nèi)容'));
- }
- build() {
- Column({ space: 5 }) {
- List() {
- ForEach(this.articleList, (item: Article) => {
- ListItem() {
- ArticleCard({ 'article': item } as Record<string, Article>)
- .margin({ top: 20 })
- }
- }, (item: Article) => 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 ArticleCard {
- @Prop article: Article;
- build() {
- Row() {
- Image($r('app.media.icon'))
- .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)
- }
- }
初始運(yùn)行效果(左圖)和手勢(shì)上滑加載后效果(右圖)如下圖所示。
在本示例中,ArticleCard組件作為ArticleListView組件的子組件,通過@Prop裝飾器接收一個(gè)Article對(duì)象,用于渲染文章卡片。
當(dāng)數(shù)據(jù)源的數(shù)組項(xiàng)為對(duì)象數(shù)據(jù)類型,并且只修改某個(gè)數(shù)組項(xiàng)的屬性值時(shí),由于數(shù)據(jù)源為復(fù)雜數(shù)據(jù)類型,ArkUI框架無法監(jiān)聽到@State裝飾器修飾的數(shù)據(jù)源數(shù)組項(xiàng)的屬性變化,從而無法觸發(fā)ForEach的重新渲染。為實(shí)現(xiàn)ForEach重新渲染,需要結(jié)合@Observed和@ObjectLink裝飾器使用。例如,在文章列表卡片上點(diǎn)擊“點(diǎn)贊”按鈕,從而修改文章的點(diǎn)贊數(shù)量。
- @Entry
- @Component
- struct ArticleListView {
- @State articleList: Array<Article> = [
- new Article('001', '第0篇文章', '文章簡(jiǎn)介內(nèi)容', false, 100),
- new Article('002', '第1篇文章', '文章簡(jiǎn)介內(nèi)容', false, 100),
- new Article('003', '第2篇文章', '文章簡(jiǎn)介內(nèi)容', false, 100),
- new Article('004', '第4篇文章', '文章簡(jiǎn)介內(nèi)容', false, 100),
- new Article('005', '第5篇文章', '文章簡(jiǎn)介內(nèi)容', false, 100),
- new Article('006', '第6篇文章', '文章簡(jiǎn)介內(nèi)容', false, 100),
- ];
- build() {
- List() {
- ForEach(this.articleList, (item: Article) => {
- ListItem() {
- ArticleCard({
- article: item
- })
- .margin({ top: 20 })
- }
- }, (item: Article) => item.id)
- }
- .padding(20)
- .scrollBar(BarState.Off)
- .backgroundColor(0xF1F3F5)
- }
- }
- @Component
- struct ArticleCard {
- @ObjectLink article: Article;
- handleLiked() {
- this.article.isLiked = !this.article.isLiked;
- this.article.likesCount = this.article.isLiked ? this.article.likesCount + 1 : this.article.likesCount - 1;
- }
- build() {
- Row() {
- Image($r('app.media.icon'))
- .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() {
- 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)
- }
- }
上述代碼的初始運(yùn)行效果(左圖)和點(diǎn)擊第1個(gè)文章卡片上的點(diǎn)贊圖標(biāo)后的運(yùn)行效果(右圖)如下圖所示。
在本示例中,Article類被@Observed裝飾器修飾。父組件ArticleListView傳入Article對(duì)象實(shí)例給子組件ArticleCard,子組件使用@ObjectLink裝飾器接收該實(shí)例。
開發(fā)者在使用ForEach的過程中,若對(duì)于鍵值生成規(guī)則的理解不夠充分,可能會(huì)出現(xiàn)錯(cuò)誤的使用方式。錯(cuò)誤使用一方面會(huì)導(dǎo)致功能層面問題,例如渲染結(jié)果非預(yù)期,另一方面會(huì)導(dǎo)致性能層面問題,例如渲染性能降低。
在本示例中,通過設(shè)置ForEach的第三個(gè)參數(shù)KeyGenerator函數(shù),自定義鍵值生成規(guī)則為數(shù)據(jù)源的索引index的字符串類型值。當(dāng)點(diǎn)擊父組件Parent中“在第1項(xiàng)后插入新項(xiàng)”文本組件后,界面會(huì)出現(xiàn)非預(yù)期的結(jié)果。
- @Entry
- @Component
- struct Parent {
- @State simpleList: Array<string> = ['one', 'two', 'three'];
- build() {
- Column() {
- Button() {
- Text('在第1項(xiàng)后插入新項(xiàng)').fontSize(30)
- }
- .onClick(() => {
- this.simpleList.splice(1, 0, 'new item');
- })
- ForEach(this.simpleList, (item: string) => {
- ChildItem({ 'item': item } as Record<string, string>)
- }, (item: string, index: number) => index.toString())
- }
- .justifyContent(FlexAlign.Center)
- .width('100%')
- .height('100%')
- .backgroundColor(0xF1F3F5)
- }
- }
- @Component
- struct ChildItem {
- @Prop item: string;
- build() {
- Text(this.item)
- .fontSize(30)
- }
- }
上述代碼的初始渲染效果(左圖)和點(diǎn)擊“在第1項(xiàng)后插入新項(xiàng)”文本組件后的渲染效果(右圖)如下圖所示。
ForEach在首次渲染時(shí),創(chuàng)建的鍵值依次為"0"、"1"、"2"。
插入新項(xiàng)后,數(shù)據(jù)源simpleList變?yōu)閇'one', 'new item', 'two', 'three'],框架監(jiān)聽到@State裝飾的數(shù)據(jù)源長(zhǎng)度變化觸發(fā)ForEach重新渲染。
ForEach依次遍歷新數(shù)據(jù)源,遍歷數(shù)據(jù)項(xiàng)"one"時(shí)生成鍵值"0",存在相同鍵值,因此不創(chuàng)建新組件。繼續(xù)遍歷數(shù)據(jù)項(xiàng)"new item"時(shí)生成鍵值"1",存在相同鍵值,因此不創(chuàng)建新組件。繼續(xù)遍歷數(shù)據(jù)項(xiàng)"two"生成鍵值"2",存在相同鍵值,因此不創(chuàng)建新組件。最后遍歷數(shù)據(jù)項(xiàng)"three"時(shí)生成鍵值"3",不存在相同鍵值,創(chuàng)建內(nèi)容為"three"的新組件并渲染。
從以上可以看出,當(dāng)最終鍵值生成規(guī)則包含index時(shí),期望的界面渲染結(jié)果為['one', 'new item', 'two', 'three'],而實(shí)際的渲染結(jié)果為['one', 'two', 'three', 'three'],渲染結(jié)果不符合開發(fā)者預(yù)期。因此,開發(fā)者在使用ForEach時(shí)應(yīng)盡量避免最終鍵值生成規(guī)則中包含index。
在本示例中,F(xiàn)orEach的第三個(gè)參數(shù)KeyGenerator函數(shù)處于缺省狀態(tài)。根據(jù)上述鍵值生成規(guī)則,此例使用框架默認(rèn)的鍵值生成規(guī)則,即最終鍵值為字符串index + '__' + JSON.stringify(item)。當(dāng)點(diǎn)擊“在第1項(xiàng)后插入新項(xiàng)”文本組件后,F(xiàn)orEach將需要為第2個(gè)數(shù)組項(xiàng)以及其后的所有項(xiàng)重新創(chuàng)建組件。
- @Entry
- @Component
- struct Parent {
- @State simpleList: Array<string> = ['one', 'two', 'three'];
- build() {
- Column() {
- Button() {
- Text('在第1項(xiàng)后插入新項(xiàng)').fontSize(30)
- }
- .onClick(() => {
- this.simpleList.splice(1, 0, 'new item');
- console.log(`[onClick]: simpleList is ${JSON.stringify(this.simpleList)}`);
- })
- ForEach(this.simpleList, (item: string) => {
- ChildItem({ 'item': item } as Record<string, string>)
- })
- }
- .justifyContent(FlexAlign.Center)
- .width('100%')
- .height('100%')
- .backgroundColor(0xF1F3F5)
- }
- }
- @Component
- struct ChildItem {
- @Prop item: string;
- aboutToAppear() {
- console.log(`[aboutToAppear]: item is ${this.item}`);
- }
- build() {
- Text(this.item)
- .fontSize(50)
- }
- }
以上代碼的初始渲染效果(左圖)和點(diǎn)擊"在第1項(xiàng)后插入新項(xiàng)"文本組件后的渲染效果(右圖)如下所示。
點(diǎn)擊“在第1項(xiàng)后插入新項(xiàng)”文本組件后,IDE的日志打印結(jié)果如下所示。
插入新項(xiàng)后,F(xiàn)orEach為new item、 two、 three三個(gè)數(shù)組項(xiàng)創(chuàng)建了對(duì)應(yīng)的組件ChildItem,并執(zhí)行了組件的aboutToAppear()生命周期函數(shù)。這是因?yàn)椋?/p>
盡管此示例中界面渲染的結(jié)果符合預(yù)期,但每次插入一條新數(shù)組項(xiàng)時(shí),F(xiàn)orEach都會(huì)為從該數(shù)組項(xiàng)起后面的所有數(shù)組項(xiàng)全部重新創(chuàng)建組件。當(dāng)數(shù)據(jù)源數(shù)據(jù)量較大或組件結(jié)構(gòu)復(fù)雜時(shí),由于組件無法得到復(fù)用,將導(dǎo)致性能體驗(yàn)不佳。因此,除非必要,否則不推薦將第三個(gè)參數(shù)KeyGenerator函數(shù)處于缺省狀態(tài),以及在鍵值生成規(guī)則中包含數(shù)據(jù)項(xiàng)索引index。
更多建議: