DEV Community

Freerain
Freerain

Posted on

鸿蒙Next状态管理最佳实践

在鸿蒙Next应用开发中,合理的状态管理是确保应用性能和响应性的关键。以下是基于最佳实践的详细阐述,每个实践都包含反例分析和正例改进,并提供了相应的代码示例。

一、使用@ObjectLink代替@Prop减少不必要的深拷贝

(一)问题场景

在父子组件数据传递时,如果子组件不需要改变传递过来的数据,使用@Prop装饰器会带来不必要的深拷贝开销,影响性能。

(二)反例分析

以下代码展示了一个父组件Parent和子组件PropChild之间的数据传递。父组件中有一个@State修饰的testClass数组,包含MyClass的实例。子组件使用@Prop接收testClass

// 反例
@Observed
class MyClass {
  public num: number = 0;
  constructor(num: number) {
    this.num = num;
  }
}

@Component
struct PropChild {
  @Prop testClass: MyClass; // @Prop会深拷贝数据
  build() {
    Text(`PropChild testNum ${this.testClass.num}`)
  }
}

@Entry
@Component
struct Parent {
  @State testClass: MyClass[] = [new MyClass(1)];
  build() {
    Column() {
      Text(`Parent testNum ${this.testClass[0].num}`)
      .onClick(() => {
          this.testClass[0].num += 1;
        })
      // PropChild没有改变@Prop testClass: MyClass的值,但@Prop会深拷贝数据,有性能开销
      PropChild({ testClass: this.testClass[0] })
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

(三)正例改进

PropChild中的@Prop改为@ObjectLink,避免了深拷贝,提高了性能。

// 正例
@Observed
class MyClass {
  public num: number = 0;
  constructor(num: number) {
    this.num = num;
  }
}

@Component
struct PropChild {
  @ObjectLink testClass: MyClass; // @ObjectLink不会深拷贝数据
  build() {
    Text(`PropChild testNum ${this.testClass.num}`)
  }
}

@Entry
@Component
struct Parent {
  @State testClass: MyClass[] = [new MyClass(1)];
  build() {
    Column() {
      Text(`Parent testNum ${this.testClass[0].num}`)
      .onClick(() => {
          this.testClass[0].num += 1;
        })
      // 子组件不需要改变数据时,使用@ObjectLink性能更好
      PropChild({ testClass: this.testClass[0] })
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

二、不使用状态变量强行更新非状态变量关联组件

(一)问题场景

开发者不应通过自定义UI状态变量来更新未被装饰为状态变量的常规变量,因为在ArkUI中,UI更新应由框架自动检测状态变量的更改来实现。

(二)反例分析

MyComponent组件中,realStateArrrealState未被装饰为状态变量,改变它们的值不会触发UI刷新,而通过needsUpdate状态变量来带动它们的更新,这种方式不合理且性能差。

// 反例
@Entry
@Component
struct MyComponent {
  @State needsUpdate: boolean = true;
  realStateArr: Array<number> = [4, 1, 3, 2]; // 未使用状态变量装饰器
  realState: Color = Color.Yellow;
  updateUIArr(param: Array<number>): Array<number> {
    const triggerAGet = this.needsUpdate;
    return param;
  }
  updateUI(param: Color): Color {
    const triggerAGet = this.needsUpdate;
    return param;
  }
  build() {
    Column({ space: 20 }) {
      ForEach(this.updateUIArr(this.realStateArr),
        (item: Array<number>) => {
          Text(`${item}`)
        })
      Text("add item")
      .onClick(() => {
          // 改变realStateArr不会触发UI视图更新
          this.realStateArr.push(this.realStateArr[this.realStateArr.length - 1] + 1);
          // 触发UI视图更新
          this.needsUpdate =!this.needsUpdate;
        })
      Text("chg color")
      .onClick(() => {
          // 改变realState不会触发UI视图更新
          this.realState = this.realState == Color.Yellow? Color.Red : Color.Yellow;
          // 触发UI视图更新
          this.needsUpdate =!this.needsUpdate;
        })
    }.backgroundColor(this.updateUI(this.realState))
    .width(200).height(500)
  }
}
Enter fullscreen mode Exit fullscreen mode

(三)正例改进

realStateArrrealState@State装饰,使其成为状态变量,改变它们的值就能直接触发UI更新。

// 正例
@Entry
@Component
struct CompA {
  @State realStateArr: Array<number> = [4, 1, 3, 2];
  @State realState: Color = Color.Yellow;
  build() {
    Column({ space: 20 }) {
      ForEach(this.realStateArr,
        (item: Array<number>) => {
          Text(`${item}`)
        })
      Text("add item")
      .onClick(() => {
          // 改变realStateArr触发UI视图更新
          this.realStateArr.push(this.realStateArr[this.realStateArr.length - 1] + 1);
        })
      Text("chg color")
      .onClick(() => {
          // 改变realState触发UI视图更新
          this.realState = this.realState == Color.Yellow? Color.Red : Color.Yellow;
        })
    }.backgroundColor(this.realState)
    .width(200).height(500)
  }
}
Enter fullscreen mode Exit fullscreen mode

三、精准控制状态变量关联的组件数

(一)问题场景

将同一状态变量绑定到多个同级组件的属性上,当状态变量改变时,所有关联组件都会刷新,即使它们的变化相同,这可能导致不必要的组件刷新,影响性能。将状态变量绑定到父组件上可以减少需要刷新的组件数,提高性能。

(二)反例分析

Page组件中,translateObjtranslateX属性被多个同级子组件(Title中的ImageTextStackButton)绑定,当translateX变化时,所有这些组件都会刷新。

// 反例
@Observed
class Translate {
  translateX: number = 20;
}

@Component
struct Title {
  @ObjectLink translateObj: Translate;
  build() {
    Row() {
      // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
      Image($r('app.media.icon'))
      .width(50)
      .height(50)
      .translate({
          x: this.translateObj.translateX // this.translateObj.translateX绑定在Image和Text组件上
        })
      Text("Title")
      .fontSize(20)
      .translate({
          x: this.translateObj.translateX
        })
    }
  }
}

@Entry
@Component
struct Page {
  @State translateObj: Translate = new Translate();
  build() {
    Column() {
      Title({
        translateObj: this.translateObj
      })
      Stack() {
      }
      .backgroundColor("black")
      .width(200)
      .height(400)
      .translate({
          x: this.translateObj.translateX // this.translateObj.translateX绑定在Stack和Button组件上
        })
      Button("move")
      .translate({
          x: this.translateObj.translateX
        })
      .onClick(() => {
          animateTo({
            duration: 50
          }, () => {
            this.translateObj.translateX = (this.translateObj.translateX + 50) % 150
          })
        })
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

(三)正例改进

将子组件共同的translate属性统一设置在父组件Column上,减少了状态变量关联的组件数。

// 正例
@Observed
class Translate {
  translateX: number = 20;
}

@Component
struct Title {
  build() {
    Row() {
      // 此处'app.media.icon'仅作示例,请开发者自行替换,否则imageSource创建失败会导致后续无法正常执行。
      Image($r('app.media.icon'))
      .width(50)
      .height(50)
      Text("Title")
      .fontSize(20)
    }
  }
}

@Entry
@Component
struct Page1 {
  @State translateObj: Translate = new Translate();
  build() {
    Column() {
      Title()
      Stack() {
      }
      .backgroundColor("black")
      .width(200)
      .height(400)
      Button("move")
      .onClick(() => {
          animateTo({
            duration: 50
          }, () => {
            this.translateObj.translateX = (this.translateObj.translateX + 50) % 150
          })
        })
    }
    .translate({ // 子组件Stack和Button设置了同一个translate属性,统一到Column上设置
        x: this.translateObj.translateX
      })
  }
}
Enter fullscreen mode Exit fullscreen mode

四、合理控制对象类型状态变量关联的组件数量

(一)问题场景

当一个复杂对象被定义为状态变量时,其任何成员属性的变化都会导致关联的所有组件刷新,即使部分组件未使用该改变的属性,这会造成“冗余刷新”,影响性能。

(二)解决方法

合理拆分复杂对象,控制其关联的组件数量,避免不必要的组件刷新。具体可参考相关文章(如文档中提到的精准控制组件的更新范围和状态管理合理使用开发指导)。

五、查询状态变量关联的组件数

(一)操作方法

在应用开发中,可以通过HiDumper查看状态变量关联的组件数,以进行性能优化。具体操作可参考状态变量组件定位工具实践。

六、避免在for、while等循环逻辑中频繁读取状态变量

(一)问题场景

在循环逻辑中频繁读取状态变量会影响性能,因为每次读取都可能触发相关的更新机制。

(二)反例分析

Index组件中,onClick事件的for循环里每次都读取@State message状态变量,这会影响性能。

// 反例
import hilog from '@ohos.hilog';

@Entry
@Component
struct Index {
  @State message: string = '';
  build() {
    Column() {
      Button('点击打印日志')
      .onClick(() => {
          for (let i = 0; i < 10; i++) {
            hilog.info(0x0000, 'TAG', '%{public}s', this.message);
          }
        })
      .width('90%')
      .backgroundColor(Color.Blue)
      .fontColor(Color.White)
      .margin({
          top: 10
        })
    }
    .justifyContent(FlexAlign.Start)
    .alignItems(HorizontalAlign.Center)
    .margin({
        top: 15
      })
  }
}
Enter fullscreen mode Exit fullscreen mode

(三)正例改进

在循环外先读取状态变量到临时变量,然后在循环中使用临时变量,减少了对状态变量的读取次数,提高了性能。

// 正例
import hilog from '@ohos.hilog';

@Entry
@Component
struct Index {
  @State message: string = '';
  build() {
    Column() {
      Button('点击打印日志')
      .onClick(() => {
          let logMessage: string = this.message;
          for (let i = 0; i < 10; i++) {
            hilog.info(0x0000, 'TAG', '%{public}s', logMessage);
          }
        })
      .width('90%')
      .backgroundColor(Color.Blue)
      .fontColor(Color.White)
      .margin({
          top: 10
        })
    }
    .justifyContent(FlexAlign.Start)
    .alignItems(HorizontalAlign.Center)
    .margin({
        top: 15
      })
  }
}
Enter fullscreen mode Exit fullscreen mode

七、建议使用临时变量替换状态变量

(一)问题场景

直接对状态变量赋值会多次触发ArkUI的查询和渲染行为,因为每次赋值都被视为状态变量的变化,这会影响性能。

(二)反例分析

Index组件的appendMsg方法中直接操作@State message状态变量,多次触发计算函数,增加了ArkUI不必要的查询和渲染,性能较差。

// 反例
import { hiTraceMeter } from '@kit.PerformanceAnalysisKit';

@Entry
@Component
struct Index {
  @State message: string = '';
  appendMsg(newMsg: string) {
    // 性能打点
    hiTraceMeter.startTrace('StateVariable', 1);
    this.message += newMsg;
    this.message += ';';
    this.message += '<br/>';
    hiTraceMeter.finishTrace('StateVariable', 1);
  }
  build() {
    Column() {
      Button('点击打印日志')
      .onClick(() => {
          this.appendMsg('操作状态变量');
        })
      .width('90%')
      .backgroundColor(Color.Blue)
      .fontColor(Color.White)
      .margin({
          top: 10
        })
    }
    .justifyContent(FlexAlign.Start)
    .alignItems(HorizontalAlign.Center)
    .margin({
        top: 15
      })
  }
}
Enter fullscreen mode Exit fullscreen mode

(三)正例改进

使用临时变量进行数据计算,最后再将计算结果赋值给状态变量,减少了ArkUI不必要的行为,提高了性能。

// 正例
import { hiTraceMeter } from '@kit.PerformanceAnalysisKit';

@Entry
@Component
struct Index {
  @State message: string = '';
  appendMsg(newMsg: string) {
    // 性能打点
    hiTraceMeter.startTrace('TemporaryVariable', 2);
    let message = this.message;
    message += newMsg;
    message += ';';
    message += '<br/>';
    this.message = message;
    hiTraceMeter.finishTrace('TemporaryVariable', 2);
  }
  build() {
    Column() {
      Button('点击打印日志')
      .onClick(() => {
          this.appendMsg('操作临时变量');
        })
      .width('90%')
      .backgroundColor(Color.Blue)
      .fontColor(Color.White)
      .margin({
          top: 10
        })
    }
    .justifyContent(FlexAlign.Start)
    .alignItems(HorizontalAlign.Center)
    .margin({
        top: 15
      })
  }
}
Enter fullscreen mode Exit fullscreen mode

通过遵循这些状态管理最佳实践,鸿蒙Next开发者能够优化应用性能,提升用户体验,确保应用在各种场景下都能高效运行。

Top comments (0)