ForEach は、配列型データに基づいてループ レンダリングを実行します。注: API バージョン 9 以降、このインターフェイスは ArkTS カードでの使用をサポートします。
1. インターフェースの説明
ForEach(
arr: any[],
itemGenerator: (item: any, index?: number) => void,
keyGenerator?: (item: any, index?: number) => string
)
2. 使用制限
ForEach はコンテナコンポーネント内で使用する必要があります。
生成される子コンポーネントは、ForEach 親コンテナ コンポーネントに含めることが許可される子コンポーネントである必要があります。
if/else 条件付きレンダリングをサブコンポーネント ジェネレーター関数に含めることができ、ForEach を if/else 条件付きレンダリング ステートメントに含めることもできます。
itemGenerator 関数の呼び出し順序は、配列内のデータ項目と必ずしも同じではないため、開発プロセス中に itemGenerator 関数と keyGenerator 関数が実行されるかどうか、およびその実行順序を想定しないでください。たとえば、次の例は正しく実行されない可能性があります。
ForEach(anArray.map((item1, index1) => { return { i: index1 + 1, data: item1 }; }),
item => Text(`${item.i}. item.data.label`),
item => item.data.id.toString())
3. 開発者の提案
開発者は項目コンストラクターの実行順序を想定しないことをお勧めします。実行順序は、配列内の項目がソートされた順序と一致しない場合があります。
配列項目が最初にレンダリングされるかどうかについて推測しないでください。ForEach の初期レンダリングでは、@Component が最初にレンダリングされるときにすべての配列項目が構築されます。この動作は、後続のフレームワーク バージョンでは遅延読み込みモードに変更される可能性があります。
Index パラメータの使用は UI 更新のパフォーマンスに重大な悪影響を与えるため、使用は避けてください。
項目コンストラクターでインデックス パラメーターを使用する場合、そのパラメーターは項目インデックス関数でも使用する必要があります。それ以外の場合、項目インデックス関数がインデックス パラメーターを使用しない場合、フレームワークは ForEach が実際のキー値を生成するときにインデックスも考慮し、インデックスはデフォルトで最後に結合されます。
4. 使用シナリオ
1. 単純な ForEach の例
arr データに基づいて 3 つの Text コンポーネントと Divide コンポーネントを作成します。
@Entry
@Component
struct MyComponent {
@State arr: number[] = [10, 20, 30];
build() {
Column({ space: 5 }) {
Button('Reverse Array')
.onClick(() => {
this.arr.reverse();
})
ForEach(this.arr, (item: number) => {
Text(`item value: ${item}`).fontSize(18)
Divider().strokeWidth(2)
}, (item: number) => item.toString())
}
}
}
2. 複雑な ForEach の例
@Component
struct CounterView {
label: string;
@State count: number = 0;
build() {
Button(`${this.label}-${this.count} click +1`)
.width(300).height(40)
.backgroundColor('#a0ffa0')
.onClick(() => {
this.count++;
})
}
}
@Entry
@Component
struct MainView {
@State arr: number[] = Array.from(Array(10).keys()); // [0.,.9]
nextUnused: number = this.arr.length;
build() {
Column() {
Button(`push new item`)
.onClick(() => {
this.arr.push(this.nextUnused++)
})
.width(300).height(40)
Button(`pop last item`)
.onClick(() => {
this.arr.pop()
})
.width(300).height(40)
Button(`prepend new item (unshift)`)
.onClick(() => {
this.arr.unshift(this.nextUnused++)
})
.width(300).height(40)
Button(`remove first item (shift)`)
.onClick(() => {
this.arr.shift()
})
.width(300).height(40)
Button(`insert at pos ${Math.floor(this.arr.length / 2)}`)
.onClick(() => {
this.arr.splice(Math.floor(this.arr.length / 2), 0, this.nextUnused++);
})
.width(300).height(40)
Button(`remove at pos ${Math.floor(this.arr.length / 2)}`)
.onClick(() => {
this.arr.splice(Math.floor(this.arr.length / 2), 1);
})
.width(300).height(40)
Button(`set at pos ${Math.floor(this.arr.length / 2)} to ${this.nextUnused}`)
.onClick(() => {
this.arr[Math.floor(this.arr.length / 2)] = this.nextUnused++;
})
.width(300).height(40)
ForEach(this.arr,
(item) => {
CounterView({ label: item.toString() })
},
(item) => item.toString()
)
}
}
MainView は、@State で装飾された数値の配列を保持します。配列項目の追加、削除、置換は監視可能な変更イベントであり、これらのイベントが発生すると、MainView 内の ForEach が更新されます。
アイテム インデックス関数は、配列アイテムごとに一意で永続的なキー値を作成します。ArkUI フレームワークは、このキー値を使用して、配列内のアイテムが変更されたかどうかを判断します。キー値が同じである限り、配列アイテムの値はは変更されていないと想定されますが、そのインデックス位置は変更される可能性があります。このメカニズムの前提は、異なる配列項目が同じキー値を持つことができないということです。
計算された ID を使用して、フレームワークは追加された配列項目、削除された配列項目、および保持された配列項目を区別できます。
(1) フレームワークは、削除された配列項目の UI コンポーネントを削除します。
(2) フレームワークは、新しく追加された配列項目の項目コンストラクターのみを実行します。
(3) フレームワークは、保持された配列項目に対して項目コンストラクターを実行しません。配列内の項目インデックスが変更された場合、フレームワークは新しい順序に従って UI コンポーネントを移動するだけで、UI コンポーネントは更新しません。
項目インデックス機能を使用することをお勧めしますが、これはオプションです。生成される ID は一意である必要があります。つまり、配列内の異なる項目に対して同じ ID を計算することはできません。2 つの配列項目が同じ値を持つ場合でも、それらの ID は異なっていなければなりません。
配列項目の値が変更された場合は、ID も変更する必要があります。
例: 前述したように、ID 生成機能はオプションです。項目インデックス関数を使用しない ForEach は次のとおりです:
ForEach(this.arr,
(item) => { CounterView({ label: item.toString() }) } )
項目 ID 関数が提供されていない場合、フレームワークは ForEach を更新するときに配列の変更をインテリジェントに検出しようとします。ただし、子コンポーネントが削除され、配列内で移動された (インデックスが変更された) 配列項目の項目コンストラクターが再実行される場合があります。上の例では、これにより、CounterView カウンターの状態に関するアプリケーションの動作が変更されます。新しい CounterView インスタンスが作成されると、counter の値は 0 に初期化されます。
3. @ObjectLink を使用した ForEach の例
繰り返しサブコンポーネントの状態を保持する必要がある場合、@ObjectLink はコンポーネント ツリー内の親コンポーネントに状態をプッシュできます。
let NextID: number = 0;
@Observed
class MyCounter {
public id: number;
public c: number;
constructor(c: number) {
this.id = NextID++;
this.c = c;
}
}
@Component
struct CounterView {
@ObjectLink counter: MyCounter;
label: string = 'CounterView';
build() {
Button(`CounterView [${this.label}] this.counter.c=${this.counter.c} +1`)
.width(200).height(50)
.onClick(() => {
this.counter.c += 1;
})
}
}
@Entry
@Component
struct MainView {
@State firstIndex: number = 0;
@State counters: Array<MyCounter> = [new MyCounter(0), new MyCounter(0), new MyCounter(0),
new MyCounter(0), new MyCounter(0)];
build() {
Column() {
ForEach(this.counters.slice(this.firstIndex, this.firstIndex + 3),
(item) => {
CounterView({ label: `Counter item #${item.id}`, counter: item })
},
(item) => item.id.toString()
)
Button(`Counters: shift up`)
.width(200).height(50)
.onClick(() => {
this.firstIndex = Math.min(this.firstIndex + 1, this.counters.length - 3);
})
Button(`counters: shift down`)
.width(200).height(50)
.onClick(() => {
this.firstIndex = Math.max(0, this.firstIndex - 1);
})
}
}
}
firstIndex の値が増加すると、Mainview 内の ForEach が更新され、項目 ID firstIndex-1 に関連付けられた CounterView サブコンポーネントが削除されます。ID firstindex + 3 の配列項目の場合、新しい CounterView サブコンポーネント インスタンスが作成されます。CounterView サブコンポーネントの状態変数カウンター値は親コンポーネント Mainview によって維持されるため、CounterView サブコンポーネント インスタンスを再構築しても状態変数カウンター値は再構築されません。
上記の配列項目 ID ルールに違反することは、実行中に繰り返し番号を追加することが容易であるため、特に配列シナリオでは最も一般的なアプリケーション開発エラーであることに注意してください。
4. ForEach をネストして使用すると
、同じコンポーネント内の別の ForEach に ForEach をネストできますが、コンポーネントを 2 つに分割し、各コンストラクターに ForEach を 1 つだけ含めることをお勧めします。以下は、ForEach ネストの反例です。
class Month {
year: number;
month: number;
days: number[];
constructor(year: number, month: number, days: number[]) {
this.year = year;
this.month = month;
this.days = days;
}
}
@Component
struct CalendarExample {
// 模拟6个月
@State calendar : Month[] = [
new Month(2020, 1, [...Array(31).keys()]),
new Month(2020, 2, [...Array(28).keys()]),
new Month(2020, 3, [...Array(31).keys()]),
new Month(2020, 4, [...Array(30).keys()]),
new Month(2020, 5, [...Array(31).keys()]),
new Month(2020, 6, [...Array(30).keys()])
]
build() {
Column() {
Button() {
Text('next month')
}.onClick(() => {
this.calendar.shift()
this.calendar.push(new Month(year: 2020, month: 7, days: [...Array(31).keys()]))
})
ForEach(this.calendar,
(item: Month) => {
ForEach(item.days,
(day : number) => {
// 构建日期块
},
(day : number) => day.toString()
)// 内部ForEach
},
(item: Month) => (item.year * 12 + item.month).toString() // 字段与年和月一起使用,作为月份的唯一ID。
)// 外部ForEach
}
}
}
上記の例には 2 つの問題があります。
(1) コードが読みにくい。
(2) 上記の年月データの配列構造形式の場合、フレームワークは配列内の月データ構造の変更 (日配列の変更など) を監視できないため、内部の ForEach は日付表示を更新できません。
アプリケーションを設計するときは、カレンダーを年、月、日のサブコンポーネントに分割することをお勧めします。日に関する情報を保持する「Day」モデル クラスを定義し、このクラスを @Observed で装飾します。DayView コンポーネントは、ObjectLink を使用して変数を装飾し、日のデータをバインドします。MonthView および Month モデル クラスに対しても同じことを行います。
5. ForEach のオプションのインデックスパラメータの使用例
コンストラクタおよび ID 生成関数でオプションのインデックスパラメータを使用できます。
@Entry
@Component
struct ForEachWithIndex {
@State arr: number[] = [4, 3, 1, 5];
build() {
Column() {
ForEach(this.arr,
(it, indx) => {
Text(`Item: ${indx} - ${it}`)
},
(it, indx) => {
return `${indx} - ${it}`
}
)
}
}
}
ID 生成関数は正しく構築する必要があります。項目コンストラクターでインデックス パラメーターが使用される場合、ID 生成関数もインデックス パラメーターを使用して、一意の ID と指定されたソース配列項目の ID を生成する必要があります。配列内の配列項目のインデックス位置が変更されると、その ID も変更されます。
この例は、インデックス パラメーターがパフォーマンスの大幅な低下を引き起こす可能性があることも示しています。項目が変更せずにソース配列内で移動された場合でも、インデックスが変更されるため、その配列項目に依存する UI は再レンダリングする必要があります。たとえば、インデックスの並べ替えを使用する場合、配列は ForEach の未変更の子 UI ノードを正しい場所に移動するだけでよく、これはフレームワークにとって軽量の操作です。インデックスを使用する場合、すべての子 UI ノードを再構築する必要があり、これは非常に重い操作になります。