鴻蒙開發(fā)-UI語法

不能在自定義組件的build()或@Builder方法里直接改變狀態(tài)變量,這可能會造成循環(huán)渲染的風險。
Text('${this.count++}')在全量更新或最小化更新會產(chǎn)生不同的影響
全量更新(API8及以前版本): ArkUI可能會陷入一個無限的重渲染的循環(huán)里,因為Text組件的每一次渲染都會改變應用的狀態(tài),就會再引起下一輪渲染的開啟。 當 this.columnColor 更改時,都會執(zhí)行整個build構建函數(shù),因此,Text(${this.count++})綁定的文本也會更改,每次重新渲染Text(${this.count++}),又會使this.count狀態(tài)變量更新,導致新一輪的build執(zhí)行,從而陷入無限循環(huán)。
最小化更新(API9-至今版本): 當 this.columnColor 更改時,只有Column組件會更新,Text組件不會更改。 只當 this.textColor 更改時,會去更新整個Text組件,其所有屬性函數(shù)都會執(zhí)行,所以會看到Text(${this.count++})自增。因為目前UI以組件為單位進行更新,如果組件上某一個屬性發(fā)生改變,會更新整體的組件。所以整體的更新鏈路是:this.textColor = Color.Pink -> Text組件整體更新->this.count++ ->Text組件整體更新。值得注意的是,這種寫法在初次渲染時會導致Text組件渲染兩次,從而對性能產(chǎn)生影響。
build函數(shù)中更改應用狀態(tài)的行為可能會比上面的示例更加隱蔽,比如:

在@Builder,@Extend或@Styles方法內(nèi)改變狀態(tài)變量 。

在計算參數(shù)時調用函數(shù)中改變應用狀態(tài)變量,例如 Text('${this.calcLabel()}')。

對當前數(shù)組做出修改,sort()改變了數(shù)組this.arr,隨后的filter方法會返回一個新的數(shù)組。
不建議在onDidBuild函數(shù)中更改狀態(tài)變量、使用animateTo等功能,這可能會導致不穩(wěn)定的UI表現(xiàn)。
不允許在aboutToDisappear函數(shù)中改變狀態(tài)變量,特別是@Link變量的修改可能會導致應用程序行為不穩(wěn)定。
不建議在生命周期aboutToDisappear內(nèi)使用async await,如果在生命周期的aboutToDisappear使用異步操作(Promise或者回調方法),自定義組件將被保留在Promise的閉包中,直到回調方法被執(zhí)行完,這個行為阻止了自定義組件的垃圾回收。

if else

動效場景下if分支切換保護失效
在動畫當中改變IfElse分支,而這個IfElse是用來做數(shù)據(jù)保護的,繼續(xù)使用該分支會導致訪問數(shù)據(jù)異常,然后造成crash。
不能在動畫中處理if else 的數(shù)據(jù)處理

解法
方式1:給數(shù)據(jù)繼續(xù)加判空的保護,即在使用data時再加一層判空,即"Text(this.data1?.str)"。
方式2:給IfElse下直接要被刪除的組件顯示的添加transition(TransitionEffect.IDENTITY)屬性,避免系統(tǒng)添加默認轉場。

反例:

class MyData {
  str: string;
  constructor(str: string) {
    this.str = str;
  }
}
@Entry
@Component
struct Index {
  @State data1: MyData|undefined = new MyData("branch 0");
  @State data2: MyData|undefined = new MyData("branch 1");


  build() {
    Column() {
      if (this.data1) {
        // 如果在動畫中增加/刪除,會給Text增加默認轉場
        // 對于刪除時,增加默認透明度轉場后,會延長組件的生命周期,Text組件沒有真正刪除,而是等轉場動畫做完后才刪除
        Text(this.data1.str)
          .id("1")
      } else if (this.data2) {
        // 如果在動畫中增加/刪除,會給Text增加默認轉場
        Text(this.data2.str)
          .id("2")
      }


      Button("play with animation")
        .onClick(() => {
          animateTo({}, ()=>{
            // 在animateTo中修改if條件,在動畫當中,會給if下的第一層組件默認轉場
            if (this.data1) {
              this.data1 = undefined;
              this.data2 = new MyData("branch 1");
            } else {
              this.data1 = new MyData("branch 0");
              this.data2 = undefined;
            }
          })
        })


      Button("play directlp")
        .onClick(() => {
          // 直接改if條件,不在動畫當中,可以正常切換,也不會加默認轉場
          if (this.data1) {
            this.data1 = undefined;
            this.data2 = new MyData("branch 1");
          } else {
            this.data1 = new MyData("branch 0");
            this.data2 = undefined;
          }
        })
    }.width("100%")
    .padding(10)
  }
}

方式1:給數(shù)據(jù)繼續(xù)加判空的保護,即在使用data時再加一層判空,即"Text(this.data1?.str)"。

class MyData {
  str: string;
  constructor(str: string) {
    this.str = str;
  }
}
@Entry
@Component
struct Index {
  @State data1: MyData|undefined = new MyData("branch 0");
  @State data2: MyData|undefined = new MyData("branch 1");


  build() {
    Column() {
      if (this.data1) {
        // 如果在動畫中增加/刪除,會給Text增加默認轉場
        // 對于刪除時,增加默認透明度轉場后,會延長組件的生命周期,Text組件沒有真正刪除,而是等轉場動畫做完后才刪除
        // 在使用數(shù)據(jù)時再加一層判空保護,如果data1存在才去使用data1當中的str
        Text(this.data1?.str)
          .id("1")
      } else if (this.data2) {
        // 如果在動畫中增加/刪除,會給Text增加默認轉場
        // 在使用數(shù)據(jù)時再加一層判空保護
        Text(this.data2?.str)
          .id("2")
      }


      Button("play with animation")
        .onClick(() => {
          animateTo({}, ()=>{
            // 在animateTo中修改if條件,在動畫當中,會給if下的第一層組件默認轉場
            if (this.data1) {
              this.data1 = undefined;
              this.data2 = new MyData("branch 1");
            } else {
              this.data1 = new MyData("branch 0");
              this.data2 = undefined;
            }
          })
        })
    }.width("100%")
    .padding(10)
  }
}

方式2:給IfElse下直接要被刪除的組件顯示的添加transition(TransitionEffect.IDENTITY)屬性,避免系統(tǒng)添加默認轉場。

class MyData {
  str: string;
  constructor(str: string) {
    this.str = str;
  }
}
@Entry
@Component
struct Index {
  @State data1: MyData|undefined = new MyData("branch 0");
  @State data2: MyData|undefined = new MyData("branch 1");


  build() {
    Column() {
      if (this.data1) {
        // 在IfElse的根組件顯示指定空的轉場效果,避免默認轉場動畫
        Text(this.data1.str)
          .transition(TransitionEffect.IDENTITY)
          .id("1")
      } else if (this.data2) {
        // 在IfElse的根組件顯示指定空的轉場效果,避免默認轉場動畫
        Text(this.data2.str)
          .transition(TransitionEffect.IDENTITY)
          .id("2")
      }


      Button("play with animation")
        .onClick(() => {
          animateTo({}, ()=>{
            // 在animateTo中修改if條件,在動畫當中,會給if下的第一層組件默認轉場
            // 但由于已經(jīng)顯示指定轉場了就不會再添加默認轉場
            if (this.data1) {
              this.data1 = undefined;
              this.data2 = new MyData("branch 1");
            } else {
              this.data1 = new MyData("branch 0");
              this.data2 = undefined;
            }
          })
        })
    }.width("100%")
    .padding(10)
  }
}

循環(huán)渲染

ForEach
在ForEach循環(huán)渲染過程中,系統(tǒng)會為每個數(shù)組元素生成一個唯一且持久的鍵值,用于標識對應的組件。當這個鍵值變化時,ArkUI框架將視為該數(shù)組元素已被替換或修改,并會基于新的鍵值創(chuàng)建一個新的組件。

非首次渲染
從本例可以看出@State 能夠監(jiān)聽到簡單數(shù)據(jù)類型數(shù)組數(shù)據(jù)源 simpleList 數(shù)組項的變化。
當 simpleList 數(shù)組項發(fā)生變化時,會觸發(fā) ForEach 進行重新渲染。
ForEach 遍歷新的數(shù)據(jù)源 ['one', 'two', 'new three'],并生成對應的鍵值one、two和new three。
其中,鍵值one和two在上次渲染中已經(jīng)存在,所以 ForEach 復用了對應的組件并進行了渲染。對于第三個數(shù)組項 "new three",由于其通過鍵值生成規(guī)則 item 生成的鍵值new three在上次渲染中不存在,因此 ForEach 為該數(shù)組項創(chuàng)建了一個新的組件。

數(shù)據(jù)源數(shù)組項發(fā)生變化。上拉加載更多

class Article {
  id: string;
  title: string;
  brief: string;


  constructor(id: string, title: string, brief: string) {
    this.id = id;
    this.title = title;
    this.brief = brief;
  }
}


@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 })
              .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() {
      // 此處'app.media.icon'僅作示例,請開發(fā)者自行替換,否則imageSource創(chuàng)建失敗會導致后續(xù)無法正常執(zhí)行。
      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)
  }
}

數(shù)據(jù)源數(shù)組項子屬性變化
當數(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ù)量。

@Observed
class Article {
  id: string;
  title: string;
  brief: string;
  isLiked: boolean;
  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 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() {
      // 此處'app.media.icon'僅作示例,請開發(fā)者自行替換,否則imageSource創(chuàng)建失敗會導致后續(xù)無法正常執(zhí)行。
      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() {
          // 此處app.media.iconLiked','app.media.iconUnLiked'僅作示例,請開發(fā)者自行替換,否則imageSource創(chuàng)建失敗會導致后續(xù)無法正常執(zhí)行。
          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)
  }
}
Article類被@Observed裝飾器修飾。父組件ArticleListView傳入Article對象實例給子組件ArticleCard,子組件使用@ObjectLink裝飾器接收該實例。

當點擊第1個文章卡片上的點贊圖標時,會觸發(fā)ArticleCard組件的handleLiked函數(shù)。該函數(shù)修改第1個卡片對應組件里article實例的isLiked和likesCount屬性值。
article實例是@ObjectLink裝飾的狀態(tài)變量,它的屬性值變化,觸發(fā)對應的ArticleCard組件渲染,讀取到的isLiked和likesCount為修改后的新值。

拖拽排序
當ForEach在List組件下使用,并且設置了onMove事件,F(xiàn)orEach每次迭代都生成一個ListItem時,可以使能拖拽排序。拖拽排序離手后,如果數(shù)據(jù)位置發(fā)生變化,則會觸發(fā)onMove事件,上報數(shù)據(jù)移動原始索引號和目標索引號。在onMove事件中,需要根據(jù)上報的起始索引號和目標索引號修改數(shù)據(jù)源。數(shù)據(jù)源修改前后,要保持每個數(shù)據(jù)的鍵值不變,只是順序發(fā)生變化,才能保證落位動畫正常執(zhí)行。

使用建議

  • 為滿足鍵值的唯一性,對于對象數(shù)據(jù)類型,建議使用對象數(shù)據(jù)中的唯一id作為鍵值。
  • 盡量避免在最終的鍵值生成規(guī)則中包含數(shù)據(jù)項索引index,以防止出現(xiàn)渲染結果非預期渲染性能降低。如果業(yè)務確實需要使用index,例如列表需要通過index進行條件渲染,開發(fā)者需要接受ForEach在改變數(shù)據(jù)源后重新創(chuàng)建組件所帶來的性能損耗。
  • 基本數(shù)據(jù)類型的數(shù)據(jù)項沒有唯一ID屬性。如果使用基本數(shù)據(jù)類型本身作為鍵值,必須確保數(shù)組項無重復。因此,對于數(shù)據(jù)源會發(fā)生變化的場景,建議將基本數(shù)據(jù)類型數(shù)組轉化為具備唯一ID屬性的對象數(shù)據(jù)類型數(shù)組,再使用唯一ID屬性作為鍵值。
  • 對于以上限制規(guī)則,index參數(shù)存在的意義為:index是開發(fā)者保證鍵值唯一性的最終手段;對數(shù)據(jù)項進行修改時,由于itemGenerator中的item參數(shù)是不可修改的,所以須用index索引值對數(shù)據(jù)源進行修改,進而觸發(fā)UI重新渲染。
  • ForEach在下列容器組件 ListGrid、Swiper以及WaterFlow 內(nèi)使用的時候,不要與LazyForEach 混用。 以List為例,同時包含F(xiàn)orEach、LazyForEach的情形是不推薦的。
  • 數(shù)組項是對象數(shù)據(jù)類型的情況下,不建議用內(nèi)容相同的數(shù)組項替換舊的數(shù)組項。如果數(shù)組項變更,但是變更前后的鍵值不變,會出現(xiàn)數(shù)據(jù)變化不渲染

不推薦案例

開發(fā)者在使用ForEach的過程中,若對于鍵值生成規(guī)則的理解不夠充分,可能會出現(xiàn)錯誤的使用方式。錯誤使用一方面會導致功能層面問題,例如渲染結果非預期,另一方面會導致性能層面問題,例如渲染性能降低。

渲染結果非預期

在本示例中,通過設置ForEach的第三個參數(shù)KeyGenerator函數(shù),自定義鍵值生成規(guī)則為數(shù)據(jù)源的索引index的字符串類型值。當點擊父組件Parent中“在第1項后插入新項”文本組件后,界面會出現(xiàn)非預期的結果。

ForEach(this.simpleList, (item: string) => {
ChildItem({ item: item })
}, (item: string) => item) // 需要保證key唯一

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容