一、开场白:装饰器选不对,性能直接废

今天咱们聊聊 HarmonyOS 里一个贼重要的功能——状态管理。

咱们写 ArkUI 应用,有个核心思想得记住:UI 就是状态的函数。状态一变,UI 就得跟着变。

ArkUI 给咱们提供了一堆装饰器,像 @Prop、@Link、@Provide、@Consume、LocalStorage 这些。变量被这些装饰器一装饰,就成了状态变量,一改就触发 UI 刷新。

听着挺方便对吧?但这里有个坑——装饰器选不对,性能直接废

我们在开发中碰到过俩典型问题:

  1. 状态和 UI 对不上:同一个状态,不同地方显示的 UI 不一样;或者 UI 展示的根本不是最新状态
  2. 瞎刷新:明明只改了一个小组件的状态,结果整个页面都刷新了,卡得一批

今天我们就把状态管理的门道给你们讲明白,帮你们避开这些坑:

  1. 咋合理选择装饰器
  2. 咋避免不必要的状态变量
  3. 咋最小化状态共享范围
  4. 咋精细化拆分复杂状态

咱们一个一个来。


二、装饰器别乱用,够用就行

1. 别给普通变量贴状态标签

状态变量管理是有开销的,不是所有变量都得用 @State 装饰。

我们踩过这个坑:一开始觉得"反正装饰一下又不会少块肉",结果性能测试时发现,多余的 @State 会让读写操作都变慢。

反例 1

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

@Component
struct MyComponent {
  @State translateObj: Translate = new Translate(); // 这玩意儿没关联任何 UI 组件,别用@State
  @State buttonMsg: string = 'I am button'; // 这玩意儿也没关联 UI 组件,也别用@State
  build() {
  }
}

说明:这俩变量跟 UI 组件半毛钱关系都没有,用了 @State 就是纯纯的性能浪费。

反例 2

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

@Component
struct MyComponent {
  @State translateObj: Translate = new Translate();
  @State buttonMsg: string = 'I am button';
  build() {
    Column() {
      Button(this.buttonMsg) // 这里只是读一下,从来不改
    }
  }
}

说明buttonMsg 这变量只读不改,你给它用 @State 干啥?读状态变量也是有成本的。

正例

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

@Component
struct UnnecessaryState1 {
  @State translateObj: Translate = new Translate(); // 这个既有读又有写,还跟 Button 关联,该用@State
  buttonMsg = 'I am button'; // 这个只读不改,普通变量就行
  build() {
    Column() {
      Button(this.buttonMsg)
        .onClick(() => {
          this.getUIContext().animateTo({
            duration: 50
          }, () => {
            this.translateObj.translateX = (this.translateObj.translateX + 50) % 150;
          })
        })
    }
    .translate({
      x: this.translateObj.translateX
    })
  }
}

我们的建议

  • 变量只读不改?用普通变量
  • 变量跟 UI 没关系?用普通变量
  • 变量既要读又要改,还驱动 UI?这才配用 @State

2. 多次修改状态?先用临时变量攒着

状态变量一变,ArkUI 就得查谁依赖它,然后刷新那些组件。

你要是连续改好几次状态变量,ArkUI 就得刷新好几次,纯纯的浪费。

反例

@Component
struct Index {
  @State message: string = '';
  
  appendMsg(newMsg: string) {
    this.message += newMsg;      // 第 1 次刷新
    this.message += ';';         // 第 2 次刷新
    this.message += '<br/>';     // 第 3 次刷新
  }
  
  build() {
    Column() {
      Button('Click Print Log')
        .onClick(() => {
          this.appendMsg('Operational state variables');
        })
    }
  }
}

看见没?appendMsg 里改了三次 message,ArkUI 就得刷新三次 UI。

正例

@Entry
@Component
struct UnnecessaryState2 {
  @State message: string = '';
  
  appendMsg(newMsg: string) {
    let message = this.message;  // 先用临时变量攒着
    message += newMsg;
    message += ';';
    message += '<br/>';
    this.message = message;      // 最后改一次状态变量
  }
  
  build() {
    Column() {
      Button('Click Print Log')
        .onClick(() => {
          this.appendMsg('Manipulating Temporary Variables');
        })
    }
  }
}

区别在哪?

  • 反例:改 3 次状态变量 → ArkUI 刷新 3 次
  • 正例:改 1 次状态变量 → ArkUI 刷新 1 次

我们管这叫"批量提交",就像数据库事务一样,攒一块儿再提交,效率高多了。


三、状态共享范围:能小就别大

咱们开发的时候,状态分两种:

  1. 组件内独享的:就这一个组件用,别的组件不关心
  2. 组件间共享的:好几个组件要用同一份数据

组件内独享的状态

这种最简单,用 @State 就行。

比如主题列表里,每个主题组件有个"是否选中"的状态,这个状态就组件自己用,别的组件不关心。

点击某个主题时,只有这个主题的组件会重新渲染,其他主题组件不受影响。

这就对了,谁用谁负责,别牵连别人


组件间共享的状态

这种就复杂点了,得分情况讨论。

三种共享场景

  1. 父子组件共享:父组件和子组件 A、子组件 B 共享一个 loading 状态

  1. 不同子树共享:左子树的孙子组件 AAA 和右子树的孙子组件 BAA 共享一个状态

  1. 不同组件树共享:组件树 A 里的子组件 AA 和组件树 B 里的孙子组件 BAA 共享状态

ArkUI 给了咱们六种方案:

装饰器组合 共享范围 生命周期
@State+@Prop 从@State 到@Prop 的整条路径 跟@State 的组件同生共死
@State+@Link 从@State 到@Link 的整条路径 跟@Link 的组件同生共死
@State+@Observed+@ObjectLink 从@State 到@ObjectLink 的整条路径 跟@ObjectLink 的组件同生共死
@Provide+@Consume @Provide 组件的整棵子树 跟@Provide 的组件绑定
LocalStorage UIAbility 内不同组件树 跟 LocalStorage 绑定
AppStorage 应用全局 跟应用进程绑定

我们的选择原则

能小就别大,能近就别远

建议优先级

@State+@Prop/Link/ObjectLink > @Provide+@Consume > LocalStorage > AppStorage

为啥?

  • 共享范围越小,性能越好
  • 数据耦合越低,维护越容易
  • 状态回收越及时,内存占用越少

四、参数层层传递?太麻烦了

按上面的优先级选装饰器,@State+@Prop/Link/ObjectLink 这三种方案得逐级往下传状态。

要是共享状态的组件隔得远,那就得一层一层传,中间经过的组件都得帮忙"传话"。

我们举个例子

"HMOS 世界 App"里有个路由状态 appNavigationStack,"DiscoverView"组件和"ResourceListView"组件都要用。

方案 1:@State+@Prop 层层传递

把状态定义在最近的公共祖先"MainPage"上,然后一层一层往下传:

MainPage (@State)
  └─ DiscoverView (@Prop)
      └─ TechArticlesView (@Prop)
          └─ ResourceListView (@Prop)

问题

哪天产品说:“哎,在 ResourceListView 的孙子组件 ActionButtonView 上也要用这个状态”。

你得改啥?

从 DiscoverView 到 ActionButtonView 路径上的 3 个组件,全得改!每个组件都得加个 @Prop 接收,再传给子组件。

累不累?累死了。

方案 2:@Provide+@Consume

在顶部组件"MainPage"上用 @Provide 注入状态:

@Provide('appNavigationStack') appNavigationStack: NavigationStack = ...;

然后后代组件想用就直接 @Consume

@Consume('appNavigationStack') appNavigationStack: NavigationStack;

优势

哪天 ActionButtonView 也要用?直接在 ActionButtonView 上加个 @Consume 就行,中间组件一个都不用改。

我们的结论

共享状态的组件要是隔得远,或者这个状态对整个组件树来说是"全局"的,别犹豫,直接用 @Provide+@Consume

代码好维护,以后扩展也方便。


五、状态复杂度不同,装饰器也得不同

@State+@Prop@State+@Link@State+@Observed+@ObjectLink 这三个优先级一样,咋选?

得看你的状态数据结构有多复杂。

三个方案对比

特性 @State+@Prop @State+@Link @State+@Observed+@ObjectLink
支持类型 所有类型 所有类型 只支持@Observed 装饰的 class
内存消耗 深拷贝,费内存 引用拷贝,省内存 -
绑定关系 单向绑定 双向绑定 只读,不能重新赋值
适用场景 非实时修改 实时修改 观察嵌套类对象属性变化

我们的选择建议

1. 要观察嵌套类对象的深层属性变化

@State+@Observed+@ObjectLink

比如你有个用户对象,里面嵌套了地址对象,地址对象里又嵌套了省份、城市。你想监听省份的变化,就得用这个方案。

2. 状态是复杂对象、类或数组

@State+@Link

复杂对象用深拷贝太费内存了,用引用拷贝省事儿。而且是双向绑定,子组件改了父组件也能同步。

3. 状态是简单数据类型

@State+@Prop@State+@Link 都行,看场景:

  • 非实时修改:比如编辑联系人信息,子组件改完不用马上同步回父组件,用 @State+@Prop
  • 实时修改:比如滚动条同步,子组件一滚父组件就得知道,用 @State+@Link

六、复杂状态要拆分,别一股脑塞 AppStorage

AppStorage 这玩意儿作用范围最广,用起来也最方便。

但我们不建议你们瞎用

为啥?

ArkUI 的状态刷新是粗粒度的。你改了一个对象的某个属性,ArkUI 会通知所有用了这个对象的组件都刷新,而不是只通知用了这个属性的组件。

我们举个例子

"HMOS 世界 App"里,用户信息 userData 包含:

  • 用户基本信息(头像、昵称、描述)
  • 用户收藏的文章 ID 列表

"我的"模块要显示用户信息,"探索"模块的文章卡片要显示用户是否点赞了当前文章。

方案 1:收藏信息作为用户信息的一个属性(❌ 不推荐)

AppStorage.setOrCreate('userData', {
  name: '张三',
  description: '这是个开发者',
  collectedIds: ['1', '2', '3']  // 收藏信息塞用户信息里
});

问题

用户在"我的"模块改了描述信息 userData.description,ArkUI 会通知所有用了 userData 的组件刷新。

结果就是:

  • "我的"模块的用户信息组件刷新 ✅ 应该的
  • "探索"模块的所有文章卡片组件也刷新 ❌ 冤枉啊,跟描述信息有啥关系?

这就是不必要的刷新,性能就这么浪费的。

方案 2:收藏信息单独存(✅ 推荐)

// 获取用户信息后,分开存
getUserData(): void {
  this.userAccountRepository.getUserData().then((data: UserData) => {
    AppStorage.setOrCreate('collectedIds', data.collectedIds);  // 收藏信息单独存
    AppStorage.setOrCreate('userData', data);                    // 用户信息单独存
  })
}

文章卡片组件只关心收藏信息:

@Component
export struct ArticleCardView {
  @StorageLink('collectedIds') collectedIds: string[] = [];  // 只绑定收藏信息
  @Prop articleItem: LearningResource = new LearningResource();

  isCollected(): boolean {
    return this.collectedIds.some((id: string) => id === this.articleItem.id);
  }

  handleCollected(): void {
    const index = this.collectedIds.findIndex((id: string) => id === this.articleItem.id);
    if (index === -1) {
      this.collectedIds.push(resourceId);
    } else {
      this.collectedIds.splice(index, 1);
    }
  }

  build() {
    ActionButtonView({
      imgResource: this.isCollected() ? $r('app.media.icon') : $r('app.media.icon'),
      count: this.articleItem.collectionCount,
      textWidth: 77
    })
    .onClick(() => {
      this.handleCollected();
    })
  }
}

优势

  • 用户信息 userData 变了 → 只刷新用 userData 的组件
  • 收藏信息 collectedIds 变了 → 只刷新用 collectedIds 的组件

互不干扰,完美。

我们的建议

别图省事把所有状态都塞 AppStorage 里。按功能模块拆分,谁用谁存,减少不必要的刷新。


七、总结一下

状态管理这事儿,记住我们这五条:

1. 避免不必要的状态变量

  • 没关联 UI 组件的变量 → 别用 @State
  • 只读不改的变量 → 别用 @State
  • 多次修改状态 → 先用临时变量攒着,最后改一次

2. 最小化状态共享范围

优先级:@State+@Prop/Link/ObjectLink > @Provide+@Consume > LocalStorage > AppStorage

能小就别大,能近就别远。

3. 减少参数层层传递

共享状态的组件要是隔得远,直接用 @Provide+@Consume,别一层一层传,累得慌。

4. 按状态复杂度选装饰器

  • 嵌套类对象深层属性变化 → @State+@Observed+@ObjectLink
  • 复杂对象、类或数组 → @State+@Link
  • 简单数据类型 → 非实时用 @State+@Prop,实时用 @State+@Link

5. 精细化拆分复杂状态

别把所有状态都塞 AppStorage 里。按功能模块拆分,谁用谁存,减少不必要的刷新。

最后一句

装饰器选不对,性能直接废。你们写代码的时候多想想,别图省事。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐