LazyForEach從提供的數(shù)據(jù)源中按需迭代數(shù)據(jù),并在每次迭代過程中創(chuàng)建相應(yīng)的組件。當(dāng)在滾動容器中使用了LazyForEach,框架會根據(jù)滾動容器可視區(qū)域按需創(chuàng)建組件,當(dāng)組件滑出可視區(qū)域外時,框架會進(jìn)行組件銷毀回收以降低內(nèi)存占用。
- LazyForEach(
- dataSource: IDataSource, // 需要進(jìn)行數(shù)據(jù)迭代的數(shù)據(jù)源
- itemGenerator: (item: any, index?: number) => void, // 子組件生成函數(shù)
- keyGenerator?: (item: any, index?: number) => string // 鍵值生成函數(shù)
- ): void
參數(shù):
參數(shù)名 | 參數(shù)類型 | 必填 | 參數(shù)描述 |
---|---|---|---|
dataSource | 是 | LazyForEach數(shù)據(jù)源,需要開發(fā)者實現(xiàn)相關(guān)接口。 | |
itemGenerator | (item: any, index?:number) => void | 是 | 子組件生成函數(shù),為數(shù)組中的每一個數(shù)據(jù)項創(chuàng)建一個子組件。 說明: item是當(dāng)前數(shù)據(jù)項,index是數(shù)據(jù)項索引值。 itemGenerator的函數(shù)體必須使用大括號{...}。itemGenerator每次迭代只能并且必須生成一個子組件。itemGenerator中可以使用if語句,但是必須保證if語句每個分支都會創(chuàng)建一個相同類型的子組件。itemGenerator中不允許使用ForEach和LazyForEach語句。 |
keyGenerator | (item: any, index?:number) => string | 否 | 鍵值生成函數(shù),用于給數(shù)據(jù)源中的每一個數(shù)據(jù)項生成唯一且固定的鍵值。當(dāng)數(shù)據(jù)項在數(shù)組中的位置更改時,其鍵值不得更改,當(dāng)數(shù)組中的數(shù)據(jù)項被新項替換時,被替換項的鍵值和新項的鍵值必須不同。鍵值生成器的功能是可選的,但是,為了使開發(fā)框架能夠更好地識別數(shù)組更改,提高性能,建議提供。如將數(shù)組反向時,如果沒有提供鍵值生成器,則LazyForEach中的所有節(jié)點(diǎn)都將重建。 說明: item是當(dāng)前數(shù)據(jù)項,index是數(shù)據(jù)項索引值。 數(shù)據(jù)源中的每一個數(shù)據(jù)項生成的鍵值不能重復(fù)。 |
- interface IDataSource {
- totalCount(): number; // 獲得數(shù)據(jù)總數(shù)
- getData(index: number): Object; // 獲取索引值對應(yīng)的數(shù)據(jù)
- registerDataChangeListener(listener: DataChangeListener): void; // 注冊數(shù)據(jù)改變的監(jiān)聽器
- unregisterDataChangeListener(listener: DataChangeListener): void; // 注銷數(shù)據(jù)改變的監(jiān)聽器
- }
接口聲明 | 參數(shù)類型 | 說明 |
---|---|---|
totalCount(): number | - | 獲得數(shù)據(jù)總數(shù)。 |
getData(index: number): any | number | 獲取索引值index對應(yīng)的數(shù)據(jù)。 index:獲取數(shù)據(jù)對應(yīng)的索引值。 |
registerDataChangeListener(listener:DataChangeListener): void | 注冊數(shù)據(jù)改變的監(jiān)聽器。 listener:數(shù)據(jù)變化監(jiān)聽器 | |
unregisterDataChangeListener(listener:DataChangeListener): void | 注銷數(shù)據(jù)改變的監(jiān)聽器。 listener:數(shù)據(jù)變化監(jiān)聽器 |
- interface DataChangeListener {
- onDataReloaded(): void; // 重新加載數(shù)據(jù)完成后調(diào)用
- onDataAdded(index: number): void; // 添加數(shù)據(jù)完成后調(diào)用
- onDataMoved(from: number, to: number): void; // 數(shù)據(jù)移動起始位置與數(shù)據(jù)移動目標(biāo)位置交換完成后調(diào)用
- onDataDeleted(index: number): void; // 刪除數(shù)據(jù)完成后調(diào)用
- onDataChanged(index: number): void; // 改變數(shù)據(jù)完成后調(diào)用
- onDataAdd(index: number): void; // 添加數(shù)據(jù)完成后調(diào)用
- onDataMove(from: number, to: number): void; // 數(shù)據(jù)移動起始位置與數(shù)據(jù)移動目標(biāo)位置交換完成后調(diào)用
- onDataDelete(index: number): void; // 刪除數(shù)據(jù)完成后調(diào)用
- onDataChange(index: number): void; // 改變數(shù)據(jù)完成后調(diào)用
- }
接口聲明 | 參數(shù)類型 | 說明 |
---|---|---|
onDataReloaded(): void | - | 通知組件重新加載所有數(shù)據(jù)。 鍵值沒有變化的數(shù)據(jù)項會使用原先的子組件,鍵值發(fā)生變化的會重建子組件。 |
onDataAdd(index: number): void8+ | number | 通知組件index的位置有數(shù)據(jù)添加。 index:數(shù)據(jù)添加位置的索引值。 |
onDataMove(from: number, to: number): void8+ | from: number, to: number | 通知組件數(shù)據(jù)有移動。 from: 數(shù)據(jù)移動起始位置,to: 數(shù)據(jù)移動目標(biāo)位置。 說明: 數(shù)據(jù)移動前后鍵值要保持不變,如果鍵值有變化,應(yīng)使用刪除數(shù)據(jù)和新增數(shù)據(jù)接口。 |
onDataDelete(index: number):void8+ | number | 通知組件刪除index位置的數(shù)據(jù)并刷新LazyForEach的展示內(nèi)容。 index:數(shù)據(jù)刪除位置的索引值。 說明: 需要保證dataSource中的對應(yīng)數(shù)據(jù)已經(jīng)在調(diào)用onDataDelete前刪除,否則頁面渲染將出現(xiàn)未定義的行為。 |
onDataChange(index: number): void8+ | number | 通知組件index的位置有數(shù)據(jù)有變化。 index:數(shù)據(jù)變化位置的索引值。 |
onDataAdded(index: number):void(deprecated) | number | 通知組件index的位置有數(shù)據(jù)添加。 從API 8開始,建議使用onDataAdd。 index:數(shù)據(jù)添加位置的索引值。 |
onDataMoved(from: number, to: number): void(deprecated) | from: number, to: number | 通知組件數(shù)據(jù)有移動。 從API 8開始,建議使用onDataMove。 from: 數(shù)據(jù)移動起始位置,to: 數(shù)據(jù)移動目標(biāo)位置。 將from和to位置的數(shù)據(jù)進(jìn)行交換。 說明: 數(shù)據(jù)移動前后鍵值要保持不變,如果鍵值有變化,應(yīng)使用刪除數(shù)據(jù)和新增數(shù)據(jù)接口。 |
onDataDeleted(index: number):void(deprecated) | number | 通知組件刪除index位置的數(shù)據(jù)并刷新LazyForEach的展示內(nèi)容。 從API 8開始,建議使用onDataDelete。 index:數(shù)據(jù)刪除位置的索引值。 |
onDataChanged(index: number): void(deprecated) | number | 通知組件index的位置有數(shù)據(jù)有變化。 從API 8開始,建議使用onDataChange。 index:數(shù)據(jù)變化監(jiān)聽器。 |
在LazyForEach循環(huán)渲染過程中,系統(tǒng)會為每個item生成一個唯一且持久的鍵值,用于標(biāo)識對應(yīng)的組件。當(dāng)這個鍵值變化時,ArkUI框架將視為該數(shù)組元素已被替換或修改,并會基于新的鍵值創(chuàng)建一個新的組件。
LazyForEach提供了一個名為keyGenerator的參數(shù),這是一個函數(shù),開發(fā)者可以通過它自定義鍵值的生成規(guī)則。如果開發(fā)者沒有定義keyGenerator函數(shù),則ArkUI框架會使用默認(rèn)的鍵值生成函數(shù),即(item: any, index: number) => { return viewId + '-' + index.toString(); }, viewId在編譯器轉(zhuǎn)換過程中生成,同一個LazyForEach組件內(nèi)其viewId是一致的。
在確定鍵值生成規(guī)則后,LazyForEach的第二個參數(shù)itemGenerator函數(shù)會根據(jù)鍵值生成規(guī)則為數(shù)據(jù)源的每個數(shù)組項創(chuàng)建組件。組件的創(chuàng)建包括兩種情況:LazyForEach首次渲染和LazyForEach非首次渲染。
在LazyForEach首次渲染時,會根據(jù)上述鍵值生成規(guī)則為數(shù)據(jù)源的每個數(shù)組項生成唯一鍵值,并創(chuàng)建相應(yīng)的組件。
- // Basic implementation of IDataSource to handle data listener
- class BasicDataSource implements IDataSource {
- private listeners: DataChangeListener[] = [];
- private originDataArray: string[] = [];
- public totalCount(): number {
- return 0;
- }
- public getData(index: number): string {
- return this.originDataArray[index];
- }
- // 該方法為框架側(cè)調(diào)用,為LazyForEach組件向其數(shù)據(jù)源處添加listener監(jiān)聽
- registerDataChangeListener(listener: DataChangeListener): void {
- if (this.listeners.indexOf(listener) < 0) {
- console.info('add listener');
- this.listeners.push(listener);
- }
- }
- // 該方法為框架側(cè)調(diào)用,為對應(yīng)的LazyForEach組件在數(shù)據(jù)源處去除listener監(jiān)聽
- unregisterDataChangeListener(listener: DataChangeListener): void {
- const pos = this.listeners.indexOf(listener);
- if (pos >= 0) {
- console.info('remove listener');
- this.listeners.splice(pos, 1);
- }
- }
- // 通知LazyForEach組件需要重載所有子組件
- notifyDataReload(): void {
- this.listeners.forEach(listener => {
- listener.onDataReloaded();
- })
- }
- // 通知LazyForEach組件需要在index對應(yīng)索引處添加子組件
- notifyDataAdd(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataAdd(index);
- })
- }
- // 通知LazyForEach組件在index對應(yīng)索引處數(shù)據(jù)有變化,需要重建該子組件
- notifyDataChange(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataChange(index);
- })
- }
- // 通知LazyForEach組件需要在index對應(yīng)索引處刪除該子組件
- notifyDataDelete(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataDelete(index);
- })
- }
- }
- class MyDataSource extends BasicDataSource {
- private dataArray: string[] = [];
- public totalCount(): number {
- return this.dataArray.length;
- }
- public getData(index: number): string {
- return this.dataArray[index];
- }
- public addData(index: number, data: string): void {
- this.dataArray.splice(index, 0, data);
- this.notifyDataAdd(index);
- }
- public pushData(data: string): void {
- this.dataArray.push(data);
- this.notifyDataAdd(this.dataArray.length - 1);
- }
- }
- @Entry
- @Component
- struct MyComponent {
- private data: MyDataSource = new MyDataSource();
- aboutToAppear() {
- for (let i = 0; i <= 20; i++) {
- this.data.pushData(`Hello ${i}`)
- }
- }
- build() {
- List({ space: 3 }) {
- LazyForEach(this.data, (item: string) => {
- ListItem() {
- Row() {
- Text(item).fontSize(50)
- .onAppear(() => {
- console.info("appear:" + item)
- })
- }.margin({ left: 10, right: 10 })
- }
- }, (item: string) => item)
- }.cachedCount(5)
- }
- }
在上述代碼中,鍵值生成規(guī)則是keyGenerator函數(shù)的返回值item。在LazyForEach循環(huán)渲染時,其為數(shù)據(jù)源數(shù)組項依次生成鍵值Hello 0、Hello 1 ... Hello 20,并創(chuàng)建對應(yīng)的ListItem子組件渲染到界面上。
運(yùn)行效果如下圖所示。
圖1 LazyForEach正常首次渲染
當(dāng)不同數(shù)據(jù)項生成的鍵值相同時,框架的行為是不可預(yù)測的。例如,在以下代碼中,LazyForEach渲染的數(shù)據(jù)項鍵值均相同,在滑動過程中,LazyForEach會對劃入劃出當(dāng)前頁面的子組件進(jìn)行預(yù)加載,而新建的子組件和銷毀的原子組件具有相同的鍵值,框架可能存在取用緩存錯誤的情況,導(dǎo)致子組件渲染有問題。
- class BasicDataSource implements IDataSource {
- private listeners: DataChangeListener[] = [];
- private originDataArray: string[] = [];
- public totalCount(): number {
- return 0;
- }
- public getData(index: number): string {
- return this.originDataArray[index];
- }
- registerDataChangeListener(listener: DataChangeListener): void {
- if (this.listeners.indexOf(listener) < 0) {
- console.info('add listener');
- this.listeners.push(listener);
- }
- }
- unregisterDataChangeListener(listener: DataChangeListener): void {
- const pos = this.listeners.indexOf(listener);
- if (pos >= 0) {
- console.info('remove listener');
- this.listeners.splice(pos, 1);
- }
- }
- notifyDataReload(): void {
- this.listeners.forEach(listener => {
- listener.onDataReloaded();
- })
- }
- notifyDataAdd(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataAdd(index);
- })
- }
- notifyDataChange(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataChange(index);
- })
- }
- notifyDataDelete(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataDelete(index);
- })
- }
- }
- class MyDataSource extends BasicDataSource {
- private dataArray: string[] = [];
- public totalCount(): number {
- return this.dataArray.length;
- }
- public getData(index: number): string {
- return this.dataArray[index];
- }
- public addData(index: number, data: string): void {
- this.dataArray.splice(index, 0, data);
- this.notifyDataAdd(index);
- }
- public pushData(data: string): void {
- this.dataArray.push(data);
- this.notifyDataAdd(this.dataArray.length - 1);
- }
- }
- @Entry
- @Component
- struct MyComponent {
- private data: MyDataSource = new MyDataSource();
- aboutToAppear() {
- for (let i = 0; i <= 20; i++) {
- this.data.pushData(`Hello ${i}`)
- }
- }
- build() {
- List({ space: 3 }) {
- LazyForEach(this.data, (item: string) => {
- ListItem() {
- Row() {
- Text(item).fontSize(50)
- .onAppear(() => {
- console.info("appear:" + item)
- })
- }.margin({ left: 10, right: 10 })
- }
- }, (item: string) => 'same key')
- }.cachedCount(5)
- }
- }
運(yùn)行效果如下圖所示。可以看到Hello 0在滑動過程中被錯誤渲染為Hello 13。
圖2 LazyForEach存在相同鍵值
當(dāng)LazyForEach數(shù)據(jù)源發(fā)生變化,需要再次渲染時,開發(fā)者應(yīng)根據(jù)數(shù)據(jù)源的變化情況調(diào)用listener對應(yīng)的接口,通知LazyForEach做相應(yīng)的更新,各使用場景如下。
- class BasicDataSource implements IDataSource {
- private listeners: DataChangeListener[] = [];
- private originDataArray: string[] = [];
- public totalCount(): number {
- return 0;
- }
- public getData(index: number): string {
- return this.originDataArray[index];
- }
- registerDataChangeListener(listener: DataChangeListener): void {
- if (this.listeners.indexOf(listener) < 0) {
- console.info('add listener');
- this.listeners.push(listener);
- }
- }
- unregisterDataChangeListener(listener: DataChangeListener): void {
- const pos = this.listeners.indexOf(listener);
- if (pos >= 0) {
- console.info('remove listener');
- this.listeners.splice(pos, 1);
- }
- }
- notifyDataReload(): void {
- this.listeners.forEach(listener => {
- listener.onDataReloaded();
- })
- }
- notifyDataAdd(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataAdd(index);
- })
- }
- notifyDataChange(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataChange(index);
- })
- }
- notifyDataDelete(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataDelete(index);
- })
- }
- }
- class MyDataSource extends BasicDataSource {
- private dataArray: string[] = [];
- public totalCount(): number {
- return this.dataArray.length;
- }
- public getData(index: number): string {
- return this.dataArray[index];
- }
- public addData(index: number, data: string): void {
- this.dataArray.splice(index, 0, data);
- this.notifyDataAdd(index);
- }
- public pushData(data: string): void {
- this.dataArray.push(data);
- this.notifyDataAdd(this.dataArray.length - 1);
- }
- }
- @Entry
- @Component
- struct MyComponent {
- private data: MyDataSource = new MyDataSource();
- aboutToAppear() {
- for (let i = 0; i <= 20; i++) {
- this.data.pushData(`Hello ${i}`)
- }
- }
- build() {
- List({ space: 3 }) {
- LazyForEach(this.data, (item: string) => {
- ListItem() {
- Row() {
- Text(item).fontSize(50)
- .onAppear(() => {
- console.info("appear:" + item)
- })
- }.margin({ left: 10, right: 10 })
- }
- .onClick(() => {
- // 點(diǎn)擊追加子組件
- this.data.pushData(`Hello ${this.data.totalCount()}`);
- })
- }, (item: string) => item)
- }.cachedCount(5)
- }
- }
當(dāng)我們點(diǎn)擊LazyForEach的子組件時,首先調(diào)用數(shù)據(jù)源data的pushData方法,該方法會在數(shù)據(jù)源末尾添加數(shù)據(jù)并調(diào)用notifyDataAdd方法。在notifyDataAdd方法內(nèi)會又調(diào)用listener.onDataAdd方法,該方法會通知LazyForEach在該處有數(shù)據(jù)添加,LazyForEach便會在該索引處新建子組件。
運(yùn)行效果如下圖所示。
圖3 LazyForEach添加數(shù)據(jù)
- class BasicDataSource implements IDataSource {
- private listeners: DataChangeListener[] = [];
- private originDataArray: string[] = [];
- public totalCount(): number {
- return 0;
- }
- public getData(index: number): string {
- return this.originDataArray[index];
- }
- registerDataChangeListener(listener: DataChangeListener): void {
- if (this.listeners.indexOf(listener) < 0) {
- console.info('add listener');
- this.listeners.push(listener);
- }
- }
- unregisterDataChangeListener(listener: DataChangeListener): void {
- const pos = this.listeners.indexOf(listener);
- if (pos >= 0) {
- console.info('remove listener');
- this.listeners.splice(pos, 1);
- }
- }
- notifyDataReload(): void {
- this.listeners.forEach(listener => {
- listener.onDataReloaded();
- })
- }
- notifyDataAdd(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataAdd(index);
- })
- }
- notifyDataChange(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataChange(index);
- })
- }
- notifyDataDelete(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataDelete(index);
- })
- }
- }
- class MyDataSource extends BasicDataSource {
- dataArray: string[] = [];
- public totalCount(): number {
- return this.dataArray.length;
- }
- public getData(index: number): string {
- return this.dataArray[index];
- }
- public addData(index: number, data: string): void {
- this.dataArray.splice(index, 0, data);
- this.notifyDataAdd(index);
- }
- public pushData(data: string): void {
- this.dataArray.push(data);
- this.notifyDataAdd(this.dataArray.length - 1);
- }
- public deleteData(index: number): void {
- this.dataArray.splice(index, 1);
- this.notifyDataDelete(index);
- }
- }
- @Entry
- @Component
- struct MyComponent {
- private data: MyDataSource = new MyDataSource();
- aboutToAppear() {
- for (let i = 0; i <= 20; i++) {
- this.data.pushData(`Hello ${i}`)
- }
- }
- build() {
- List({ space: 3 }) {
- LazyForEach(this.data, (item: string, index: number) => {
- ListItem() {
- Row() {
- Text(item).fontSize(50)
- .onAppear(() => {
- console.info("appear:" + item)
- })
- }.margin({ left: 10, right: 10 })
- }
- .onClick(() => {
- // 點(diǎn)擊刪除子組件
- this.data.deleteData(this.data.dataArray.indexOf(item));
- })
- }, (item: string) => item)
- }.cachedCount(5)
- }
- }
當(dāng)我們點(diǎn)擊LazyForEach的子組件時,首先調(diào)用數(shù)據(jù)源data的deleteData方法,該方法會刪除數(shù)據(jù)源對應(yīng)索引處的數(shù)據(jù)并調(diào)用notifyDatDelete方法。在notifyDataDelete方法內(nèi)會又調(diào)用listener.onDataDelete方法,該方法會通知LazyForEach在該處有數(shù)據(jù)刪除,LazyForEach便會在該索引處刪除對應(yīng)子組件。
運(yùn)行效果如下圖所示。
圖4 LazyForEach刪除數(shù)據(jù)
- class BasicDataSource implements IDataSource {
- private listeners: DataChangeListener[] = [];
- private originDataArray: string[] = [];
- public totalCount(): number {
- return 0;
- }
- public getData(index: number): string {
- return this.originDataArray[index];
- }
- registerDataChangeListener(listener: DataChangeListener): void {
- if (this.listeners.indexOf(listener) < 0) {
- console.info('add listener');
- this.listeners.push(listener);
- }
- }
- unregisterDataChangeListener(listener: DataChangeListener): void {
- const pos = this.listeners.indexOf(listener);
- if (pos >= 0) {
- console.info('remove listener');
- this.listeners.splice(pos, 1);
- }
- }
- notifyDataReload(): void {
- this.listeners.forEach(listener => {
- listener.onDataReloaded();
- })
- }
- notifyDataAdd(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataAdd(index);
- })
- }
- notifyDataChange(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataChange(index);
- })
- }
- notifyDataDelete(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataDelete(index);
- })
- }
- }
- class MyDataSource extends BasicDataSource {
- private dataArray: string[] = [];
- public totalCount(): number {
- return this.dataArray.length;
- }
- public getData(index: number): string {
- return this.dataArray[index];
- }
- public addData(index: number, data: string): void {
- this.dataArray.splice(index, 0, data);
- this.notifyDataAdd(index);
- }
- public pushData(data: string): void {
- this.dataArray.push(data);
- this.notifyDataAdd(this.dataArray.length - 1);
- }
- public deleteData(index: number): void {
- this.dataArray.splice(index, 1);
- this.notifyDataDelete(index);
- }
- public changeData(index: number, data: string): void {
- this.dataArray.splice(index, 1, data);
- this.notifyDataChange(index);
- }
- }
- @Entry
- @Component
- struct MyComponent {
- private moved: number[] = [];
- private data: MyDataSource = new MyDataSource();
- aboutToAppear() {
- for (let i = 0; i <= 20; i++) {
- this.data.pushData(`Hello ${i}`)
- }
- }
- build() {
- List({ space: 3 }) {
- LazyForEach(this.data, (item: string, index: number) => {
- ListItem() {
- Row() {
- Text(item).fontSize(50)
- .onAppear(() => {
- console.info("appear:" + item)
- })
- }.margin({ left: 10, right: 10 })
- }
- .onClick(() => {
- this.data.changeData(index, item + '00');
- })
- }, (item: string) => item)
- }.cachedCount(5)
- }
- }
當(dāng)我們點(diǎn)擊LazyForEach的子組件時,首先改變當(dāng)前數(shù)據(jù),然后調(diào)用數(shù)據(jù)源data的changeData方法,在該方法內(nèi)會調(diào)用notifyDataChange方法。在notifyDataChange方法內(nèi)會又調(diào)用listener.onDataChange方法,該方法通知LazyForEach組件該處有數(shù)據(jù)發(fā)生變化,LazyForEach便會在對應(yīng)索引處重建子組件。
運(yùn)行效果如下圖所示。
圖5 LazyForEach改變單個數(shù)據(jù)
- class BasicDataSource implements IDataSource {
- private listeners: DataChangeListener[] = [];
- private originDataArray: string[] = [];
- public totalCount(): number {
- return 0;
- }
- public getData(index: number): string {
- return this.originDataArray[index];
- }
- registerDataChangeListener(listener: DataChangeListener): void {
- if (this.listeners.indexOf(listener) < 0) {
- console.info('add listener');
- this.listeners.push(listener);
- }
- }
- unregisterDataChangeListener(listener: DataChangeListener): void {
- const pos = this.listeners.indexOf(listener);
- if (pos >= 0) {
- console.info('remove listener');
- this.listeners.splice(pos, 1);
- }
- }
- notifyDataReload(): void {
- this.listeners.forEach(listener => {
- listener.onDataReloaded();
- })
- }
- notifyDataAdd(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataAdd(index);
- })
- }
- notifyDataChange(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataChange(index);
- })
- }
- notifyDataDelete(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataDelete(index);
- })
- }
- }
- class MyDataSource extends BasicDataSource {
- private dataArray: string[] = [];
- public totalCount(): number {
- return this.dataArray.length;
- }
- public getData(index: number): string {
- return this.dataArray[index];
- }
- public addData(index: number, data: string): void {
- this.dataArray.splice(index, 0, data);
- this.notifyDataAdd(index);
- }
- public pushData(data: string): void {
- this.dataArray.push(data);
- this.notifyDataAdd(this.dataArray.length - 1);
- }
- public deleteData(index: number): void {
- this.dataArray.splice(index, 1);
- this.notifyDataDelete(index);
- }
- public changeData(index: number): void {
- this.notifyDataChange(index);
- }
- public reloadData(): void {
- this.notifyDataReload();
- }
- public modifyAllData(): void {
- this.dataArray = this.dataArray.map((item: string) => {
- return item + '0';
- })
- }
- }
- @Entry
- @Component
- struct MyComponent {
- private moved: number[] = [];
- private data: MyDataSource = new MyDataSource();
- aboutToAppear() {
- for (let i = 0; i <= 20; i++) {
- this.data.pushData(`Hello ${i}`)
- }
- }
- build() {
- List({ space: 3 }) {
- LazyForEach(this.data, (item: string, index: number) => {
- ListItem() {
- Row() {
- Text(item).fontSize(50)
- .onAppear(() => {
- console.info("appear:" + item)
- })
- }.margin({ left: 10, right: 10 })
- }
- .onClick(() => {
- this.data.modifyAllData();
- this.data.reloadData();
- })
- }, (item: string) => item)
- }.cachedCount(5)
- }
- }
當(dāng)我們點(diǎn)擊LazyForEach的子組件時,首先調(diào)用data的modifyAllData方法改變了數(shù)據(jù)源中的所有數(shù)據(jù),然后調(diào)用數(shù)據(jù)源的reloadData方法,在該方法內(nèi)會調(diào)用notifyDataReload方法。在notifyDataReload方法內(nèi)會又調(diào)用listener.onDataReloaded方法,通知LazyForEach需要重建所有子節(jié)點(diǎn)。LazyForEach會將原所有數(shù)據(jù)項和新所有數(shù)據(jù)項一一做鍵值比對,若有相同鍵值則使用緩存,若鍵值不同則重新構(gòu)建。
運(yùn)行效果如下圖所示。
圖6 LazyForEach改變多個數(shù)據(jù)
若僅靠LazyForEach的刷新機(jī)制,當(dāng)item變化時若想更新子組件,需要將原來的子組件全部銷毀再重新構(gòu)建,在子組件結(jié)構(gòu)較為復(fù)雜的情況下,靠改變鍵值去刷新渲染性能較低。因此框架提供了@Observed與@ObjectLink機(jī)制進(jìn)行深度觀測,可以做到僅刷新使用了該屬性的組件,提高渲染性能。開發(fā)者可根據(jù)其自身業(yè)務(wù)特點(diǎn)選擇使用哪種刷新方式。
- class BasicDataSource implements IDataSource {
- private listeners: DataChangeListener[] = [];
- private originDataArray: StringData[] = [];
- public totalCount(): number {
- return 0;
- }
- public getData(index: number): StringData {
- return this.originDataArray[index];
- }
- registerDataChangeListener(listener: DataChangeListener): void {
- if (this.listeners.indexOf(listener) < 0) {
- console.info('add listener');
- this.listeners.push(listener);
- }
- }
- unregisterDataChangeListener(listener: DataChangeListener): void {
- const pos = this.listeners.indexOf(listener);
- if (pos >= 0) {
- console.info('remove listener');
- this.listeners.splice(pos, 1);
- }
- }
- notifyDataReload(): void {
- this.listeners.forEach(listener => {
- listener.onDataReloaded();
- })
- }
- notifyDataAdd(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataAdd(index);
- })
- }
- notifyDataChange(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataChange(index);
- })
- }
- notifyDataDelete(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataDelete(index);
- })
- }
- }
- class MyDataSource extends BasicDataSource {
- private dataArray: StringData[] = [];
- public totalCount(): number {
- return this.dataArray.length;
- }
- public getData(index: number): StringData {
- return this.dataArray[index];
- }
- public addData(index: number, data: StringData): void {
- this.dataArray.splice(index, 0, data);
- this.notifyDataAdd(index);
- }
- public pushData(data: StringData): void {
- this.dataArray.push(data);
- this.notifyDataAdd(this.dataArray.length - 1);
- }
- }
- @Observed
- class StringData {
- message: string;
- constructor(message: string) {
- this.message = message;
- }
- }
- @Entry
- @Component
- struct MyComponent {
- private moved: number[] = [];
- @State data: MyDataSource = new MyDataSource();
- aboutToAppear() {
- for (let i = 0; i <= 20; i++) {
- this.data.pushData(new StringData(`Hello ${i}`));
- }
- }
- build() {
- List({ space: 3 }) {
- LazyForEach(this.data, (item: StringData, index: number) => {
- ListItem() {
- ChildComponent({data: item})
- }
- .onClick(() => {
- item.message += '0';
- })
- }, (item: StringData, index: number) => index.toString())
- }.cachedCount(5)
- }
- }
- @Component
- struct ChildComponent {
- @ObjectLink data: StringData
- build() {
- Row() {
- Text(this.data.message).fontSize(50)
- .onAppear(() => {
- console.info("appear:" + this.data.message)
- })
- }.margin({ left: 10, right: 10 })
- }
- }
此時點(diǎn)擊LazyForEach子組件改變item.message時,重渲染依賴的是ChildComponent的@ObjectLink成員變量對其子屬性的監(jiān)聽,此時框架只會刷新Text(this.data.message),不會去重建整個ListItem子組件。
圖7 LazyForEach改變數(shù)據(jù)子屬性
- class BasicDataSource implements IDataSource {
- private listeners: DataChangeListener[] = [];
- private originDataArray: string[] = [];
- public totalCount(): number {
- return 0;
- }
- public getData(index: number): string {
- return this.originDataArray[index];
- }
- registerDataChangeListener(listener: DataChangeListener): void {
- if (this.listeners.indexOf(listener) < 0) {
- console.info('add listener');
- this.listeners.push(listener);
- }
- }
- unregisterDataChangeListener(listener: DataChangeListener): void {
- const pos = this.listeners.indexOf(listener);
- if (pos >= 0) {
- console.info('remove listener');
- this.listeners.splice(pos, 1);
- }
- }
- notifyDataReload(): void {
- this.listeners.forEach(listener => {
- listener.onDataReloaded();
- })
- }
- notifyDataAdd(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataAdd(index);
- })
- }
- notifyDataChange(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataChange(index);
- })
- }
- notifyDataDelete(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataDelete(index);
- })
- }
- }
- class MyDataSource extends BasicDataSource {
- private dataArray: string[] = [];
- public totalCount(): number {
- return this.dataArray.length;
- }
- public getData(index: number): string {
- return this.dataArray[index];
- }
- public addData(index: number, data: string): void {
- this.dataArray.splice(index, 0, data);
- this.notifyDataAdd(index);
- }
- public pushData(data: string): void {
- this.dataArray.push(data);
- this.notifyDataAdd(this.dataArray.length - 1);
- }
- public deleteData(index: number): void {
- this.dataArray.splice(index, 1);
- this.notifyDataDelete(index);
- }
- }
- @Entry
- @Component
- struct MyComponent {
- private data: MyDataSource = new MyDataSource();
- aboutToAppear() {
- for (let i = 0; i <= 20; i++) {
- this.data.pushData(`Hello ${i}`)
- }
- }
- build() {
- List({ space: 3 }) {
- LazyForEach(this.data, (item: string, index: number) => {
- ListItem() {
- Row() {
- Text(item).fontSize(50)
- .onAppear(() => {
- console.info("appear:" + item)
- })
- }.margin({ left: 10, right: 10 })
- }
- .onClick(() => {
- // 點(diǎn)擊刪除子組件
- this.data.deleteData(index);
- })
- }, (item: string) => item)
- }.cachedCount(5)
- }
- }
圖8 LazyForEach刪除數(shù)據(jù)非預(yù)期
當(dāng)我們多次點(diǎn)擊子組件時,會發(fā)現(xiàn)刪除的并不一定是我們點(diǎn)擊的那個子組件。原因是當(dāng)我們刪除了某一個子組件后,位于該子組件對應(yīng)的數(shù)據(jù)項之后的各數(shù)據(jù)項,其index均應(yīng)減1,但實際上后續(xù)的數(shù)據(jù)項對應(yīng)的子組件仍然使用的是最初分配的index,其itemGenerator中的index并沒有發(fā)生變化,所以刪除結(jié)果和預(yù)期不符。
修復(fù)代碼如下所示。
- class BasicDataSource implements IDataSource {
- private listeners: DataChangeListener[] = [];
- private originDataArray: string[] = [];
- public totalCount(): number {
- return 0;
- }
- public getData(index: number): string {
- return this.originDataArray[index];
- }
- registerDataChangeListener(listener: DataChangeListener): void {
- if (this.listeners.indexOf(listener) < 0) {
- console.info('add listener');
- this.listeners.push(listener);
- }
- }
- unregisterDataChangeListener(listener: DataChangeListener): void {
- const pos = this.listeners.indexOf(listener);
- if (pos >= 0) {
- console.info('remove listener');
- this.listeners.splice(pos, 1);
- }
- }
- notifyDataReload(): void {
- this.listeners.forEach(listener => {
- listener.onDataReloaded();
- })
- }
- notifyDataAdd(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataAdd(index);
- })
- }
- notifyDataChange(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataChange(index);
- })
- }
- notifyDataDelete(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataDelete(index);
- })
- }
- }
- class MyDataSource extends BasicDataSource {
- private dataArray: string[] = [];
- public totalCount(): number {
- return this.dataArray.length;
- }
- public getData(index: number): string {
- return this.dataArray[index];
- }
- public addData(index: number, data: string): void {
- this.dataArray.splice(index, 0, data);
- this.notifyDataAdd(index);
- }
- public pushData(data: string): void {
- this.dataArray.push(data);
- this.notifyDataAdd(this.dataArray.length - 1);
- }
- public deleteData(index: number): void {
- this.dataArray.splice(index, 1);
- this.notifyDataDelete(index);
- }
- public reloadData(): void {
- this.notifyDataReload();
- }
- }
- @Entry
- @Component
- struct MyComponent {
- private data: MyDataSource = new MyDataSource();
- aboutToAppear() {
- for (let i = 0; i <= 20; i++) {
- this.data.pushData(`Hello ${i}`)
- }
- }
- build() {
- List({ space: 3 }) {
- LazyForEach(this.data, (item: string, index: number) => {
- ListItem() {
- Row() {
- Text(item).fontSize(50)
- .onAppear(() => {
- console.info("appear:" + item)
- })
- }.margin({ left: 10, right: 10 })
- }
- .onClick(() => {
- // 點(diǎn)擊刪除子組件
- this.data.deleteData(index);
- // 重置所有子組件的index索引
- this.data.reloadData();
- })
- }, (item: string, index: number) => item + index.toString())
- }.cachedCount(5)
- }
- }
在刪除一個數(shù)據(jù)項后調(diào)用reloadData方法,重建后面的數(shù)據(jù)項,以達(dá)到更新index索引的目的。
圖9 修復(fù)LazyForEach刪除數(shù)據(jù)非預(yù)期
- class BasicDataSource implements IDataSource {
- private listeners: DataChangeListener[] = [];
- private originDataArray: StringData[] = [];
- public totalCount(): number {
- return 0;
- }
- public getData(index: number): StringData {
- return this.originDataArray[index];
- }
- registerDataChangeListener(listener: DataChangeListener): void {
- if (this.listeners.indexOf(listener) < 0) {
- console.info('add listener');
- this.listeners.push(listener);
- }
- }
- unregisterDataChangeListener(listener: DataChangeListener): void {
- const pos = this.listeners.indexOf(listener);
- if (pos >= 0) {
- console.info('remove listener');
- this.listeners.splice(pos, 1);
- }
- }
- notifyDataReload(): void {
- this.listeners.forEach(listener => {
- listener.onDataReloaded();
- })
- }
- notifyDataAdd(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataAdd(index);
- })
- }
- notifyDataChange(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataChange(index);
- })
- }
- notifyDataDelete(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataDelete(index);
- })
- }
- }
- class MyDataSource extends BasicDataSource {
- private dataArray: StringData[] = [];
- public totalCount(): number {
- return this.dataArray.length;
- }
- public getData(index: number): StringData {
- return this.dataArray[index];
- }
- public addData(index: number, data: StringData): void {
- this.dataArray.splice(index, 0, data);
- this.notifyDataAdd(index);
- }
- public pushData(data: StringData): void {
- this.dataArray.push(data);
- this.notifyDataAdd(this.dataArray.length - 1);
- }
- public reloadData(): void {
- this.notifyDataReload();
- }
- }
- class StringData {
- message: string;
- imgSrc: Resource;
- constructor(message: string, imgSrc: Resource) {
- this.message = message;
- this.imgSrc = imgSrc;
- }
- }
- @Entry
- @Component
- struct MyComponent {
- private moved: number[] = [];
- private data: MyDataSource = new MyDataSource();
- aboutToAppear() {
- for (let i = 0; i <= 20; i++) {
- this.data.pushData(new StringData(`Hello ${i}`, $r('app.media.img')));
- }
- }
- build() {
- List({ space: 3 }) {
- LazyForEach(this.data, (item: StringData, index: number) => {
- ListItem() {
- Column() {
- Text(item.message).fontSize(50)
- .onAppear(() => {
- console.info("appear:" + item.message)
- })
- Image(item.imgSrc)
- .width(500)
- .height(200)
- }.margin({ left: 10, right: 10 })
- }
- .onClick(() => {
- item.message += '00';
- this.data.reloadData();
- })
- }, (item: StringData, index: number) => JSON.stringify(item))
- }.cachedCount(5)
- }
- }
圖10 LazyForEach僅改變文字但是圖片閃爍問題
在我們點(diǎn)擊ListItem子組件時,我們只改變了數(shù)據(jù)項的message屬性,但是LazyForEach的刷新機(jī)制會導(dǎo)致整個ListItem被重建。由于Image組件是異步刷新,所以視覺上圖片會發(fā)生閃爍。為了解決這種情況我們應(yīng)該使用@ObjectLink和@Observed去單獨(dú)刷新使用了item.message的Text組件。
修復(fù)代碼如下所示。
- class BasicDataSource implements IDataSource {
- private listeners: DataChangeListener[] = [];
- private originDataArray: StringData[] = [];
- public totalCount(): number {
- return 0;
- }
- public getData(index: number): StringData {
- return this.originDataArray[index];
- }
- registerDataChangeListener(listener: DataChangeListener): void {
- if (this.listeners.indexOf(listener) < 0) {
- console.info('add listener');
- this.listeners.push(listener);
- }
- }
- unregisterDataChangeListener(listener: DataChangeListener): void {
- const pos = this.listeners.indexOf(listener);
- if (pos >= 0) {
- console.info('remove listener');
- this.listeners.splice(pos, 1);
- }
- }
- notifyDataReload(): void {
- this.listeners.forEach(listener => {
- listener.onDataReloaded();
- })
- }
- notifyDataAdd(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataAdd(index);
- })
- }
- notifyDataChange(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataChange(index);
- })
- }
- notifyDataDelete(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataDelete(index);
- })
- }
- }
- class MyDataSource extends BasicDataSource {
- private dataArray: StringData[] = [];
- public totalCount(): number {
- return this.dataArray.length;
- }
- public getData(index: number): StringData {
- return this.dataArray[index];
- }
- public addData(index: number, data: StringData): void {
- this.dataArray.splice(index, 0, data);
- this.notifyDataAdd(index);
- }
- public pushData(data: StringData): void {
- this.dataArray.push(data);
- this.notifyDataAdd(this.dataArray.length - 1);
- }
- }
- @Observed
- class StringData {
- message: string;
- imgSrc: Resource;
- constructor(message: string, imgSrc: Resource) {
- this.message = message;
- this.imgSrc = imgSrc;
- }
- }
- @Entry
- @Component
- struct MyComponent {
- @State data: MyDataSource = new MyDataSource();
- aboutToAppear() {
- for (let i = 0; i <= 20; i++) {
- this.data.pushData(new StringData(`Hello ${i}`, $r('app.media.img')));
- }
- }
- build() {
- List({ space: 3 }) {
- LazyForEach(this.data, (item: StringData, index: number) => {
- ListItem() {
- ChildComponent({data: item})
- }
- .onClick(() => {
- item.message += '0';
- })
- }, (item: StringData, index: number) => index.toString())
- }.cachedCount(5)
- }
- }
- @Component
- struct ChildComponent {
- @ObjectLink data: StringData
- build() {
- Column() {
- Text(this.data.message).fontSize(50)
- .onAppear(() => {
- console.info("appear:" + this.data.message)
- })
- Image(this.data.imgSrc)
- .width(500)
- .height(200)
- }.margin({ left: 10, right: 10 })
- }
- }
圖11 修復(fù)LazyForEach僅改變文字但是圖片閃爍問題
- class BasicDataSource implements IDataSource {
- private listeners: DataChangeListener[] = [];
- private originDataArray: StringData[] = [];
- public totalCount(): number {
- return 0;
- }
- public getData(index: number): StringData {
- return this.originDataArray[index];
- }
- registerDataChangeListener(listener: DataChangeListener): void {
- if (this.listeners.indexOf(listener) < 0) {
- console.info('add listener');
- this.listeners.push(listener);
- }
- }
- unregisterDataChangeListener(listener: DataChangeListener): void {
- const pos = this.listeners.indexOf(listener);
- if (pos >= 0) {
- console.info('remove listener');
- this.listeners.splice(pos, 1);
- }
- }
- notifyDataReload(): void {
- this.listeners.forEach(listener => {
- listener.onDataReloaded();
- })
- }
- notifyDataAdd(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataAdd(index);
- })
- }
- notifyDataChange(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataChange(index);
- })
- }
- notifyDataDelete(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataDelete(index);
- })
- }
- }
- class MyDataSource extends BasicDataSource {
- private dataArray: StringData[] = [];
- public totalCount(): number {
- return this.dataArray.length;
- }
- public getData(index: number): StringData {
- return this.dataArray[index];
- }
- public addData(index: number, data: StringData): void {
- this.dataArray.splice(index, 0, data);
- this.notifyDataAdd(index);
- }
- public pushData(data: StringData): void {
- this.dataArray.push(data);
- this.notifyDataAdd(this.dataArray.length - 1);
- }
- }
- @Observed
- class StringData {
- message: NestedString;
- constructor(message: NestedString) {
- this.message = message;
- }
- }
- @Observed
- class NestedString {
- message: string;
- constructor(message: string) {
- this.message = message;
- }
- }
- @Entry
- @Component
- struct MyComponent {
- private moved: number[] = [];
- @State data: MyDataSource = new MyDataSource();
- aboutToAppear() {
- for (let i = 0; i <= 20; i++) {
- this.data.pushData(new StringData(new NestedString(`Hello ${i}`)));
- }
- }
- build() {
- List({ space: 3 }) {
- LazyForEach(this.data, (item: StringData, index: number) => {
- ListItem() {
- ChildComponent({data: item})
- }
- .onClick(() => {
- item.message.message += '0';
- })
- }, (item: StringData, index: number) => item.toString() + index.toString())
- }.cachedCount(5)
- }
- }
- @Component
- struct ChildComponent {
- @ObjectLink data: StringData
- build() {
- Row() {
- Text(this.data.message.message).fontSize(50)
- .onAppear(() => {
- console.info("appear:" + this.data.message.message)
- })
- }.margin({ left: 10, right: 10 })
- }
- }
圖12 ObjectLink屬性變化后UI未更新
@ObjectLink裝飾的成員變量僅能監(jiān)聽到其子屬性的變化,再深入嵌套的屬性便無法觀測到了,因此我們只能改變它的子屬性去通知對應(yīng)組件重新渲染,具體請查看@ObjectLink與@Observed的詳細(xì)使用方法和限制條件。
修復(fù)代碼如下所示。
- class BasicDataSource implements IDataSource {
- private listeners: DataChangeListener[] = [];
- private originDataArray: StringData[] = [];
- public totalCount(): number {
- return 0;
- }
- public getData(index: number): StringData {
- return this.originDataArray[index];
- }
- registerDataChangeListener(listener: DataChangeListener): void {
- if (this.listeners.indexOf(listener) < 0) {
- console.info('add listener');
- this.listeners.push(listener);
- }
- }
- unregisterDataChangeListener(listener: DataChangeListener): void {
- const pos = this.listeners.indexOf(listener);
- if (pos >= 0) {
- console.info('remove listener');
- this.listeners.splice(pos, 1);
- }
- }
- notifyDataReload(): void {
- this.listeners.forEach(listener => {
- listener.onDataReloaded();
- })
- }
- notifyDataAdd(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataAdd(index);
- })
- }
- notifyDataChange(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataChange(index);
- })
- }
- notifyDataDelete(index: number): void {
- this.listeners.forEach(listener => {
- listener.onDataDelete(index);
- })
- }
- }
- class MyDataSource extends BasicDataSource {
- private dataArray: StringData[] = [];
- public totalCount(): number {
- return this.dataArray.length;
- }
- public getData(index: number): StringData {
- return this.dataArray[index];
- }
- public addData(index: number, data: StringData): void {
- this.dataArray.splice(index, 0, data);
- this.notifyDataAdd(index);
- }
- public pushData(data: StringData): void {
- this.dataArray.push(data);
- this.notifyDataAdd(this.dataArray.length - 1);
- }
- }
- @Observed
- class StringData {
- message: NestedString;
- constructor(message: NestedString) {
- this.message = message;
- }
- }
- @Observed
- class NestedString {
- message: string;
- constructor(message: string) {
- this.message = message;
- }
- }
- @Entry
- @Component
- struct MyComponent {
- private moved: number[] = [];
- @State data: MyDataSource = new MyDataSource();
- aboutToAppear() {
- for (let i = 0; i <= 20; i++) {
- this.data.pushData(new StringData(new NestedString(`Hello ${i}`)));
- }
- }
- build() {
- List({ space: 3 }) {
- LazyForEach(this.data, (item: StringData, index: number) => {
- ListItem() {
- ChildComponent({data: item})
- }
- .onClick(() => {
- item.message = new NestedString(item.message.message + '0');
- })
- }, (item: StringData, index: number) => item.toString() + index.toString())
- }.cachedCount(5)
- }
- }
- @Component
- struct ChildComponent {
- @ObjectLink data: StringData
- build() {
- Row() {
- Text(this.data.message.message).fontSize(50)
- .onAppear(() => {
- console.info("appear:" + this.data.message.message)
- })
- }.margin({ left: 10, right: 10 })
- }
- }
圖13 修復(fù)ObjectLink屬性變化后UI更新
更多建議: