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