刚接手一个项目做性能优化,发现一个特别诡异的现象:用户在一个二级页面的搜索框里敲字,结果底层列表的整个列表项都在疯狂重绘;或者用户点击了底部的点赞按钮,整个页面像是闪了一下,全都刷新了一遍。

打开 ArkUI Inspector,看谁在频繁重建,发现很多根本没有用到的组件,也跟着凑热闹。排查到最后,问题全指向了一个地方:AppStorage 的滥用。

很多开发者习惯了把什么状态都往 AppStorage 里塞。以为它是个全局的万能存储,其实它是个“大喇叭”。只要你往里面写数据,所有监听这个 key 的组件都会收到通知。一传十,十传百,整个页面的渲染流水线都被拖垮了。

分清 AppStorage 和 LocalStorage 的界限

AppStorage 的定位是应用级的全局状态。适合放什么?比如登录用户的 Token、系统的深色浅色模式、全局的字体大小。这些东西一旦改变,全应用都得跟着反应。

而页面上某个列表的滚动位置、某个输入框的文字、某个组件的展开折叠状态,这属于页面级状态。你把它放进 AppStorage,简直是用大炮打蚊子,还会误伤友军。

这时候该用 LocalStorage。它的作用域限制在当前 UIAbility 的页面实例内。

typescript

// 1. 创建一个页面级的 LocalStorage 实例
const storage = new LocalStorage({ 'searchText': '' });

@Entry(storage)
@Component
struct SearchPage {
  // 2. 在页面内使用 @LocalStorageLink
  @LocalStorageLink('searchText') searchText: string = '';

  build() {
    Column() {
      TextInput({ text: this.searchText })
        .onChange((value) => {
          // 只会更新 SearchPage 及其子组件,不会污染全局
          this.searchText = value;
        })
      
      SearchResultList() // 这个组件如果没有 @LocalStorageLink,就不会被通知
    }
  }
}

关键语句是 @Entry(storage) 和 @LocalStorageLink。这样绑定之后,searchText 的改变只会驱动 SearchPage 树里的对应节点刷新,应用的其它层级和页面完全不知道发生了什么。执行结果就是,用户在输入框敲字,页面帧率稳如老狗。

拆解大对象与 @StorageProp 的只读策略

除了滥用存储,还有个常犯的错误:把巨大的对象塞进 AppStorage。

假设你有一个包含几百条数据的商品列表 products,你把它存进了 AppStorage。然后在某个子组件里,你只想读取这个列表的长度显示在标题栏上。

如果你用的是 @StorageLink,当列表里的某一件商品发生变化时,整个 products 对象都会被标记为脏数据。所有引用了它的组件都会被重新渲染。

如果这个组件只是读取数据而不修改,务必使用 @StorageProp。它建立的是一种单向的同步机制。

typescript

// 父组件写入
AppStorage.setOrCreate('productCount', 100);

@Component
struct HeaderTitle {
  // 子组件只读,不会反向修改,也不会因为深层对象变化而崩溃
  @StorageProp('productCount') count: number = 0;

  build() {
    Text(`共 ${this.count} 件商品`)
  }
}

@StorageProp 会让组件读取 AppStorage 的值,如果值变了,组件刷新;但是组件内部不能直接赋值修改它(如果修改,只会改本地副本,不会同步回 AppStorage)。这样极大降低了双向数据流带来的不可控刷新。

至于那些列表数据,应该在页面的 @State 或者 @Local 里维护。只有需要跨页面共享的少量关键标识,才往 AppStorage 扔。

用 @Watch 精准监控状态变动

有些时候,你确实需要监听全局状态的变化,然后去干点别的事情,比如发网络请求,或者做埋点。不要傻乎乎地在 build() 里做,build() 会被执行无数次,你的请求也会发无数次。

用 @Watch 装饰器。

typescript

@Component
struct ThemeAwareComponent {
  // 当 themeMode 改变时,触发 onThemeChange 方法
  @StorageLink('themeMode') @Watch('onThemeChange') themeMode: string = 'light';

  onThemeChange(propName: string) {
    console.info(`主题变更为: ${this.themeMode}`);
    // 执行相应的业务逻辑,比如重新请求皮肤配置
  }

  build() {
    // UI 渲染逻辑
  }
}

@Watch 回调是在状态变更后触发的,你可以在这里拿到旧值和新值做对比,避免无效操作。

解决状态管理引发的频繁渲染,核心在于收拢作用域。能用 @Local 的不用 @State,能用 LocalStorage 的不用 AppStorage,能单向传值的不用双向绑定。把数据流向理清楚,你的应用才不会像一只惊弓之鸟,稍微碰一下就浑身乱颤。

Logo

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

更多推荐