概述

做 ArkUI 开发的人几乎都踩过同一个坑:组件上 `@State` 装饰了一个对象数组,列表也能正常显示,可在某个 `onClick` 回调里把数组里某个元素的属性改了——`this.list[0].votes += 1`,日志里值明明变了,页面却纹丝不动。

这是状态管理 V1 的观测边界在起作用:`@State` 默认只能看到"第一层"的数据变化,数组元素内部的属性属于更深的层,默认不在它的观测范围内。谁改动了它,页面就不会响应。本文先讲清楚这个边界到底划在哪里、为什么这么划,再给出一个可运行的修复范式,最后补充几个容易翻车的写法。

 

说明:观测边界是怎么划的

V1 的 `@State` 按"层级"决定自己能看到什么,规则大致如下:

- 类型为 `string / number / boolean` 的变量:直接赋值(`this.count = 1`)能被观测到;
- 类型为 `Object / class` 的变量:**整体替换**(`this.user = new User(...)`)以及第一层属性的赋值(`this.user.age = 18`)能被观测到;
- 类型为 `Array` 的变量:数组本身的赋值、`push`、`splice`、按下标整体替换元素(`this.arr[0] = item`)能被观测到;
- **对象的属性**再往下走,比如数组里的元素(本身是对象)的某个字段——`this.arr[0].votes`——就属于第二层,V1 默认观测不到。

也就是说,V1 的状态观测是"浅"的,它以 `@State` 声明的那个变量为原点,只对第一层变化敏感。数组元素对象内部的字段变化,本质是拿"引用"去改了堆里的数据,引用本身没有变,`@State` 无从感知。这也解释了为什么"打印值是对的,UI 却不刷新"——数据确实被改了,只是没人通知组件。

要让深层变化也被看见,官方给出的是另一套机制:给数据类打上`@Observed`,让被观测对象变成"可观测类",再由子组件用`@ObjectLink`接收这个对象。`@Observed` 类会在属性被改写时主动广播,`@ObjectLink` 在子组件里建立对该对象的引用级观测,改的是同一个对象,刷新的却是绑定它的那个组件。这套组合拳把"深层对象"观测能力外扩了一级,是处理列表项内部数据交互的标准姿势。

顺带说一句,在新版的 V2 状态管理中(`@ObservedV2` + `@Trace`),这种深层观测已经可以做到"直接写、自动刷新",不必再拆子组件,这也是 V2 在 V1 之上的一个明显改进。

 

使用实践:一个投票列表的修复过程

场景是这样的:首页有一个投票列表,每个条目显示名称和票数,点"+1"按钮票数加一。最直觉的写法是被观测边界拦住的,这里给出的是正确范式——把"条目对象变成 `@Observed` 类 + 条目交给子组件 `@ObjectLink` 接收",深层修改就有了响应。

@Observed
class VoteItem {
  id: number;
  name: string;
  votes: number;

  constructor(id: number, name: string, votes: number) {
    this.id = id;
    this.name = name;
    this.votes = votes;
  }
}

@Component
struct VoteCard {
  @ObjectLink item: VoteItem;

  build() {
    Row() {
      Text(this.item.name).fontSize(16)
      Text(`${this.item.votes} 票`).fontSize(14)
      Button('+1')
        .onClick(() => { this.item.votes++; })
    }
  }
}

@Entry
@Component
struct VotePage {
  @State list: VoteItem[] = [
    new VoteItem(1, '方案A', 0),
    new VoteItem(2, '方案B', 0)
  ];

  build() {
    Column() {
      ForEach(this.list, (item: VoteItem) => {
        VoteCard({ item: item })
      }, (item: VoteItem) => item.id.toString())
    }
  }
}

对照文本里的错误写法看几个关键点:

1. 别在父组件里改元素内部属性。 `this.list[0].votes++` 这种写法值会变、UI 不动,是被观测边界拦住的典例;如果临时图快,可以退而求其次整体替换该元素,如 `this.list.splice(index, 1, newItem)`,但这属于"绕过",语义上不够优雅。
2. `@Observed` 必须标在数据类上。漏标这一类,`@ObjectLink` 就无从观测。子组件里 `@ObjectLink item` 只能接收来自状态变量(`@State` 数组)里取出的可观测对象,不能在子组件里 `new` 一个再传给它。
3. `ForEach` 的 key 。 这里用 `item.id` 而不是 `index`,后续如果涉及元素的增删或顺序变化,`id` 当 key 才能让列表按数据走正确的增量更新,避免整个列表重建。

这样改完之后,在每个条目的回调里修改的都是同一个 `@Observed` 实例的属性,改动会被广播到 `VoteCard`,UI 立即响应——那个"改了没反应"的诡异问题,到此结束。

Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐