文档管理中心
我的

状态管理V1和V2适用场景介绍

问题现象

状态管理存在V1和V2版本,那在实际开发过程中,状态管理是推荐用V1开发还是V2开发?

背景知识

ArkUI提供了状态管理(V1)和状态管理(V2)用于应用开发中状态变量的管理。状态管理V1使用代理观察数据,当创建一个状态变量时,同时也创建了一个数据代理观察者。状态管理V2将观察能力增强到数据本身,数据本身就是可观察的,更改数据会触发相应的视图的更新。两种状态管理方式存在的一些差异,可参考:状态管理V1与V2能力对比、状态管理V1现状以及V2优点、V1状态变量更新和V2状态变量更新差异。

解决方案

针对应用开发过程中常见的场景,汇总如下表:

展开

场景

状态管理选择说明

深度状态观测

为了对嵌套类对象属性直接观测,推荐使用V2。

计算属性重复计算

为了避免重用属性而重复计算导致的性能问题,推荐使用V2。

状态变量修改监听

为了对状态变量具有深度监听的能力,且能获取到变化前的值,推荐使用V2。

使用animateTo实现动画

由于在状态管理V2中使用animateTo可能出现动画效果异常,推荐使用V1,API22以后可以通过applySync/flushUpdates/flushUIUpdates接口:同步刷新辅助方法规避。

使用高级组件

由于在低版本API中状态管理V2中未提供部分高级组件,推荐使用V1。 从API version 18开始,系统预置UI组件支持在状态管理V2组件中使用。

自定义局部页面主题

由于在低版本API中状态管理V2中未提供WithTheme组件,推荐使用V1。从API version 18开始,该接口支持在状态管理V2组件中使用。

自定义组件复用

由于在低版本API中状态管理V2中未提供@Reusable装饰器,推荐使用V1。从API version 18开始,该装饰器支持在状态管理V2组件中使用。

一、状态管理V2的优势与适用场景。

  1. 深度状态观测: V2通过@ObservedV2和@Trace装饰器,支持对嵌套类对象属性的直接观测,无需像V1那样依赖@ObjectLink和自定义组件封装。例如,当修改对象的深层属性(如obj.a.b)时,V2能自动触发UI更新,而V1需要手动处理或依赖额外逻辑。
    场景案例对比如下:
    • 使用V1监控嵌套类对象属性的变化:
      @Observed
      class Father {
        son: Son;
      
        constructor(name: string, age: number) {
          this.son = new Son(name, age);
        }
      }
      
      @Observed
      class Son {
        name: string;
        age: number;
      
        constructor(name: string, age: number) {
          this.name = name;
          this.age = age;
        }
      }
      
      @Component
      struct Child {
        // 使用@ObjectLink装饰器与自定义组件来实现观测
        @ObjectLink son: Son;
      
        build() {
          Row() {
            Column() {
              Text(`name: ${this.son.name} age: ${this.son.age}`)
                .fontSize(50)
                .fontWeight(FontWeight.Bold)
                .onClick(() => {
                  this.son.age++;
                });
            }
            .width('100%');
          }
          .height('100%');
        }
      }
      
      @Component
      export struct NestedV1 {
        @State father: Father = new Father('John', 8);
      
        build() {
          Column() {
            Text('使用V1监控嵌套类对象属性的变化')
              .fontSize(20)
              .fontWeight(FontWeight.Bold);
            Child({ son: this.father.son });
          }
          .width('100%')
          .height('100%');
        }
      }
    • 使用V2监控嵌套类对象属性的变化:
      @ObservedV2
      class Son {
        // 在嵌套类中使用@Trace装饰的属性具有被观测变化的能力
        @Trace age: number = 100;
        name: string;
      
        constructor(name: string, age: number) {
          this.name = name;
          this.age = age;
        }
      }
      
      class Father {
        son: Son;
      
        constructor(name: string, age: number) {
          this.son = new Son(name, age);
        }
      }
      
      @ComponentV2
      export struct NestedV2 {
        father: Father = new Father('John', 8);
      
        build() {
          Column() {
            Text('使用V2监控嵌套类对象属性的变化')
              .fontSize(20)
              .fontWeight(FontWeight.Bold);
            Row() {
              Column() {
                Text(`name: ${this.father.son.name} age: ${this.father.son.age}`)
                  .fontSize(50)
                  .fontWeight(FontWeight.Bold)
                  .onClick(() => {
                    this.father.son.age++;
                  });
              }
              .width('100%');
            }
            .height('100%');
          }
          .height('100%')
          .width('100%')
          .height('100%');
        }
      }

    总结:通过V1这种方式虽然能够实现对嵌套类中属性变化的观测,但是当嵌套层级较深时,代码将会变得十分复杂,易用性差。采用V2的话,使用@ObservedV2装饰的嵌套类中被@Trace装饰的属性具有被观测变化的能力,当该属性值变化时,会触发该属性绑定的UI组件刷新,代码较简洁。

  2. 属性重复计算优化: V2通过@Computed计算属性在被计算的值变化的时候,只会计算一次,解决V1中多次重用该属性从而重复计算导致的性能问题。
    场景案例对比如下:
    • 使用V1重用属性导致重复计算:
      @Component
      export struct CalculationV1 {
        firstName: string = 'Li';
        @State lastName: string = 'Hua';
      
        build() {
          Column() {
            Text('使用V1重用属性导致重复计算')
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
              .margin({ bottom: 50 });
      
            // 两个Text组件UI重复计算
            Text(this.firstName + ' ' + this.lastName)
              .margin({ bottom: 8 });
            Text(this.firstName + ' ' + this.lastName);
            Button('changed lastName')
              .margin({ top: 16 })
              .onClick(() => {
                this.lastName += 'a';
              });
          }
          .width('100%')
          .height('100%');
        }
      }
    • 使用V2计算属性解决重复计算:
      @ComponentV2
      export struct CalculationV2 {
        @Local firstName: string = 'Li';
        @Local lastName: string = 'Hua';
      
        @Computed
        get fullName() {
          console.info('Computed');
          return this.firstName + ' ' + this.lastName;
        }
      
        build() {
          Column() {
            Text('使用V2计算属性解决重复计算')
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
              .margin({ bottom: 50 });
      
            // this.fullName被绑定在两个Text组件上,计算只发生了一次
            Text(this.fullName)
              .margin({ bottom: 8 });
            Text(this.fullName);
            Button('changed lastName')
              .margin({ top: 16 })
              .onClick(() => {
                this.lastName += 'a';
              });
          }
          .width('100%')
          .height('100%');
        }
      }

    总结:通过V1这种方式,对于前两个Text组件,this.lastName + ' '+ this.firstName这段逻辑被求解了两次。如果UI中有多处重复计算,会产生较大的性能开销。采用V2的话,this.fullName被绑定在两个Text组件上,计算只发生了一次,解决UI多次重用该属性从而重复计算导致的性能问题。

  3. 状态变量修改监听: V2引入的@Monitor状态变量修改监听装饰器,使得状态变量具有深度监听的能力。现有状态管理V1版本无法实现对对象、数组中某一单个属性或数组项变化的监听,且无法获取变化前的值。而V2中@Monitor可以监听深层属性的变化,并能够根据更改前后的值处理。场景案例对比如下:
    • 使用V1中@Watch监听状态变量修改:
      @Observed
      class Info {
        name: string = 'Tom';
        age: number = 25;
      }
      
      @Component
      export struct ModificationV1 {
        @State @Watch('onInfoChange') info: Info = new Info();
      
        // 更改info中的name或age属性时,均会触发info注册的@Watch回调
        onInfoChange() {
          console.info(`info after change name: ${this.info.name}, age: ${this.info.age} `);
        }
      
        build() {
          Column() {
            Text('使用V1中@Watch监听状态变量修改')
              .fontSize(20)
              .fontWeight(FontWeight.Bold);
            Row() {
              Column() {
                Text(`name: ${this.info.name}, age: ${this.info.age}`);
                Button('change info name')
                  .margin({ top: 16 })
                  .onClick(() => {
                    this.info.name = 'Jack';
                  });
                Button('change info age')
                  .margin({ top: 16 })
                  .onClick(() => {
                    this.info.age = 30;
                  });
              }
              .width('100%');
            }
            .height('100%');
          }
          .width('100%')
          .height('100%');
        }
      }
    • 使用V2中@Monitor监听状态变量修改:
      @ObservedV2
      class Info {
        @Trace name: string = 'Tom';
        @Trace age: number = 25;
      
        @Monitor('name')
        // 更改name属性时触发此回调
        onNameChange(monitor: IMonitor) {
          console.info(`name change from ${monitor.value()?.before} to ${monitor.value()?.now}`);
        }
      
        @Monitor('age')
        // 更改age属性时触发此回调
        onAgeChange(monitor: IMonitor) {
          console.info(`age change from ${monitor.value()?.before} to ${monitor.value()?.now}`);
        }
      
        constructor(name: string, age: number) {
          this.name = name;
          this.age = age;
        }
      }
      
      @ComponentV2
      export struct ModificationV2 {
        @Local info: Info = new Info('Tom', 25);
      
        build() {
          Column() {
            Text('使用V2中@Monitor监听状态变量修改')
              .fontSize(20)
              .fontWeight(FontWeight.Bold);
            Row() {
              Column() {
                Text(`name: ${this.info.name}, age: ${this.info.age}`);
                Button('change info name')
                  .margin({ top: 16 })
                  .onClick(() => {
                    this.info.name = 'Jack';
                  });
                Button('change info age')
                  .margin({ top: 16 })
                  .onClick(() => {
                    this.info.age = 30;
                  });
              }
              .width('100%');
            }
            .height('100%');
          }
          .width('100%')
          .height('100%');
        }
      }

    总结:通过V1这种方式,更改info中的name或age属性时,均会触发info注册的@Watch回调。在这个回调中,由于无法获取数据更改前的值,在业务逻辑更加复杂的场景下,无法准确知道是哪一个属性或元素发生了变化而触发了@Watch事件,这不便于开发者对变量的更改进行准确监听。而对于通过V2的实现方式,准确监听了对象中每个属性的变化及其变化前后的值。

  4. 更灵活的装饰器设计: V2提出使用@Local替代@State,明确区分组件内部状态(不可由外部初始化)。采用@Param表示组件从外部传入的状态,使得父子组件之间的数据能够进行同步,统一了V1中@Prop、@Link等复杂的传参逻辑。为了实现子组件向父组件要求更新@Param装饰变量的能力,V2还提出了@Event装饰器。场景案例对比如下:
    • 使用V1在父子组件之间同步数据:
      @Component
      export struct DecoratorV1 {
        @State title: string = 'Title One';
        @State fontColor: Color = Color.Black;
      
        build() {
          Column() {
            Text('使用V1在父子组件之间同步数据')
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
              .margin({ bottom: 50 });
            Child({
              title: this.title,
              fontColor: this.fontColor
            });
          }
          .width('100%')
          .height('100%');
        }
      }
      
      @Component
      struct Child {
        // @Link实现了父子组件双向数据绑定,子组件可以直接修改父组件的状态,可能会导致状态流向不清晰
        @Link title: string;
        @Prop fontColor: Color = Color.Red;
      
        build() {
          Column() {
            Text(`${this.title}`)
              .fontColor(this.fontColor);
            Button('change to Title Two')
              .margin({ top: 16 })
              .onClick(() => {
                this.changeFactory(2);
              });
            Button('change to Title One')
              .margin({ top: 16 })
              .onClick(() => {
                this.changeFactory(1);
              });
          };
        }
      
        changeFactory(type: number) {
          if (type == 1) {
            this.title = 'Title One';
          } else if (type == 2) {
            this.title = 'Title Two';
          }
        }
      }
    • 使用V2在父子组件之间同步数据:
      @ComponentV2
      export struct DecoratorV2 {
        @Local title: string = 'Title One';
        @Local fontColor: Color = Color.Black;
      
        build() {
          Column() {
            Text('使用V2在父子组件之间同步数据')
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
              .margin({ bottom: 50 });
            Child({
              title: this.title,
              fontColor: this.fontColor,
              changeFactory: (type: number) => {
                if (type == 1) {
                  this.title = 'Title One';
                } else if (type == 2) {
                  this.title = 'Title Two';
                }
              }
            });
          }
          .width('100%')
          .height('100%');
        }
      }
      
      @ComponentV2
      struct Child {
        // @Param用于单向数据传递,子组件无法直接修改父组件的状态
        @Param title: string = '';
        @Param fontColor: Color = Color.Red;
        // 实现父子组件双向同步的能力,通过事件驱动的方式实现,状态流向更加明确
        @Event changeFactory: (x: number) => void = () => {
        };
      
        build() {
          Column() {
            Text(`${this.title}`)
              .fontColor(this.fontColor);
            Button('change to Title Two')
              .margin({ top: 16 })
              .onClick(() => {
                this.changeFactory(2);
              });
            Button('change to Title One')
              .margin({ top: 16 })
              .onClick(() => {
                this.changeFactory(1);
              });
          };
        }
      }

    总结:V1通过@Link实现了父子组件双向数据绑定,子组件可以直接修改父组件的状态,无需额外的事件处理,但可能会导致状态流向不清晰,尤其是在复杂组件中,维护成本较高。而V2使用@Param用于单向数据传递,子组件无法直接修改父组件的状态。为了实现父子组件双向同步的能力,通过事件驱动的方式实现,状态流向更加明确,代码结构更加清晰,维护成本较低。

完整示例:以上代码以自定义组件形式出现,想要成功运行,可参考以下入口示例测试。

import { NestedV1 } from '../views/Scene1/NestedV1';
import { NestedV2 } from '../views/Scene1/NestedV2';
import { CalculationV1 } from '../views/Scene2/CalculationV1';
import { CalculationV2 } from '../views/Scene2/CalculationV2';
import { ModificationV1 } from '../views/Scene3/ModificationV1';
import { ModificationV2 } from '../views/Scene3/ModificationV2';
import { DecoratorV1 } from '../views/Scene4/DecoratorV1';
import { DecoratorV2 } from '../views/Scene4/DecoratorV2';

@Entry
@Component
struct MainPage {
  @State currentIndexOut: number = 0;
  @State currentIndexIn1: number = 0;
  @State currentIndexIn2: number = 0;
  @State currentIndexIn3: number = 0;
  @State currentIndexIn4: number = 0;

  build() {
    Tabs({ index: this.currentIndexOut }) {
      TabContent() {
        Tabs() {
          TabContent() {
            NestedV1();
          }.tabBar('V1');

          TabContent() {
            NestedV2();
          }.tabBar('V2');
        }
        .height('100%')
        .width('100%')
        .scrollable(false)
        .barMode(BarMode.Fixed)
        .barBackgroundColor('#ffffff')
        .barPosition(BarPosition.Start)
        .onChange((index: number) => {
          this.currentIndexIn1 = index;
        });
      }.tabBar('场景一');

      TabContent() {
        Tabs() {
          TabContent() {
            CalculationV1();
          }.tabBar('V1');

          TabContent() {
            CalculationV2();
          }.tabBar('V2');
        }
        .height('100%')
        .width('100%')
        .scrollable(false)
        .barMode(BarMode.Fixed)
        .barBackgroundColor('#ffffff')
        .barPosition(BarPosition.Start)
        .onChange((index: number) => {
          this.currentIndexIn2 = index;
        });
      }.tabBar('场景二');

      TabContent() {
        Tabs() {
          TabContent() {
            ModificationV1();
          }.tabBar('V1');

          TabContent() {
            ModificationV2();
          }.tabBar('V2');
        }
        .height('100%')
        .width('100%')
        .scrollable(false)
        .barMode(BarMode.Fixed)
        .barBackgroundColor('#ffffff')
        .barPosition(BarPosition.Start)
        .onChange((index: number) => {
          this.currentIndexIn3 = index;
        });
      }.tabBar('场景三');

      TabContent() {
        Tabs() {
          TabContent() {
            DecoratorV1();
          }.tabBar('V1');

          TabContent() {
            DecoratorV2();
          }.tabBar('V2');
        }
        .height('100%')
        .width('100%')
        .scrollable(false)
        .barMode(BarMode.Fixed)
        .barBackgroundColor('#ffffff')
        .barPosition(BarPosition.Start)
        .onChange((index: number) => {
          this.currentIndexIn4 = index;
        });
      }.tabBar('场景四');
    }
    .height('100%')
    .width('100%')
    .scrollable(false)
    .barMode(BarMode.Fixed)
    .barBackgroundColor('#ffffff')
    .barPosition(BarPosition.End)
    .onChange((index: number) => {
      this.currentIndexOut = index;
    });
  }
}

二、状态管理V2的局限性及注意事项。

  1. 部分场景不支持animateTo、keyframeAnimateTo动画: 详情可参考文档在状态管理V2中使用animateTo动画效果异常、动效场景。
  2. 在低版本API中高级组件缺失: V2版本的部分高级功能在低版本API中未提供,仍在持续优化。例如:DownloadFileButton、ProgressButton、SegmentButton。从API version 18开始,系统预置UI组件支持在状态管理V2组件中使用。
  3. 在低版本API中无法自定义局部页面主题: V2版本在低版本API未提供WithTheme组件,因此无法设置应用局部页面自定义主题风格。从API version 18开始,该接口支持在状态管理V2组件中使用。
  4. 在低版本API中组件复用能力受限: V2版本在低版本API未提供@Reusable装饰器,无法复用自定义组件。因此,如果大量自定义组件从组件树上被移除,组件和其对应的JSView对象无法被放入复用缓存中,后续创建新自定义组件节点时,会消耗大量组件重新创建的时间。从API version 18开始,可以使用@ReusableV2装饰@ComponentV2装饰的自定义组件。

三、状态管理混用与迁移场景介绍。

  1. 混用场景介绍。

    从实际角度出发,V1提出时间较早,有很大的基础,开发者无法一次性迁移成V2,因此在V1的代码中使用V2的部分能力是允许的,V2中也没有完全禁止V1。状态管理V1与V2的混用开发可以参考状态管理V1V2混用文档。API19新增的makeV1Observed以及enableV2Compatibility接口完善了V1与V2混用能力。

  2. 迁移场景介绍。

    对希望将现有V1应用迁移到V2的开发者,可参考V1V2能力对比及迁移简表进行迁移,更详细的装饰器迁移示例可参考状态管理V1向V2迁移场景。

常见FAQ

Q:@Event能实现的功能@Param都能实现,那为什么还要引入@Event?

A:一般来说,@Param主要用于变量传递,@Event用于函数传递,数据流向不一样,@Param是父传子,子组件不能更改父组件传递的数据,@Event是触发父组件更新数据。即使@Param传递的是函数,也必须赋初始值,而@Event不用初始化。

Q:AppStorage和AppStorageV2以及PersistentStorage和PersistentStorageV2是互通的吗?

A:AppStorage与AppStorageV2之间的数据互不共享,详情请参考AppStorageV2: 应用全局UI状态存储。

没有PersistentStorageV2,只有PersistenceV2,PersistentStorage和PersistenceV2存在差别,PersistentStorage功能上耦合了AppStorage,并且数据在不同module中使用也会有问题,因此推荐使用PersistenceV2的globalConnect接口替换掉PersistentStorage的persistProp接口。