本节摘要:V2 是状态管理的第二代体系,用 @ObservedV2/@Trace 实现属性级精确观察、用 @Local 替代 @State、@Param 替代 @Prop。本节用同一案例的双体系改写做对比,给出新项目与存量项目的选择清单。
先看 V1 体系的三个痛点,它们恰好是 V2 的设计动机。痛点一,观察粒度粗:4.1 节验证过 @State 看不到嵌套对象内部变化,于是要用 @Observed+@ObjectLink 这对组合拳补救,类的作者与使用者都要记得配合,忘了一处就是"界面不刷新"的疑难杂症。痛点二,装饰器语义隐晦:@Prop 收到的是拷贝、@Link 是引用、@ObjectLink 只能接 @Observed 类——规则靠背,不靠类型系统。痛点三,跨组件的精确刷新难做:@Provide 是组件级的,一旦提供,消费侧整段 build 都算依赖,粒度控制有限。
V2 把这三个痛点正面解决。核心是 @ObservedV2 与 @Trace 这对装饰器:类标 @ObservedV2 后,被 @Trace 标注的属性获得属性级观察能力——无论嵌套多深,只要链路上都是 V2 类,改哪个属性就刷新读它的那个片段。同一个 Goods 例子改写成 V2:
@ObservedV2 class Goods { @Trace name: string = '' @Trace price: number = 0 @Trace checked: boolean = false } @ObservedV2 class Cart { @Trace items: Goods[] = [] @Computed get selectedCount(): number { return this.items.filter(i => i.checked).length } }
注意两处新东西:其一是 @Computed,把"已选件数"这种派生值声明为计算属性,依赖变化自动重算,购物车里那段手写回调同步的代码可以删掉;其二是 items 数组本身也被 @Trace,数组内部增删同样精确观察——V1 里"数组 push 能不能触发刷新"的模糊地带没有了。
组件侧的对应关系一表看清:
| V1 | V2 | 语义变化 |
|---|---|---|
| @State | @Local | 仅组件内部状态,命名直白 |
| @Prop | @Param | 父传子,默认不可本地修改 |
| @Link | @Param + @Event | 双向拆成"值加回调",数据流显式化 |
| @Provide/@Consume | @Provider/@Consumer | 同名跨层,语义不变 |
| @Observed+@ObjectLink | @ObservedV2+@Trace | 属性级观察,无需接收方配合 |
其中 @Param + @Event 的拆分最值得展开。V1 的 @Link 把"子改父"藏在双向绑定里,出了问题不知道改发生在哪一侧;V2 要求显式声明 @Event 回调,子的修改必须经由回调通知父——代码多两行,数据流却从"隐式双向"变成"显式往返",这与前端社区"单向数据流 + 事件上抛"的主流实践殊途同归。
把 4.2 的购物车结算条用 V2 重写关键部分:
@Entry @ComponentV2 struct CartPageV2 { cart: Cart = new Cart() @Local vip: boolean = false build() { Column({ space: 16 }) { List() { Repeat(this.cart.items) .each((row) => { ListItem() { GoodsRowV2({ goods: row.item }) } }) .key((item, idx) => `${item.name}-${idx}`) } .layoutWeight(1) Text(`已选 ${this.cart.selectedCount} 件 · ${this.vip ? '会员价生效' : '原价'}`) .fontSize(18) } .onAppear(() => { this.cart.items.push(new Goods('无线键盘', 299)) this.cart.items.push(new Goods('显示器支架', 159)) }) } } @ComponentV2 struct GoodsRowV2 { @Param goods: Goods = new Goods('', 0) @Event('onCheck') onCheck: (v: boolean) => void = () => {} build() { Row({ space: 12 }) { Toggle({ type: ToggleType.Checkbox, isOn: this.goods.checked }) .onChange((v: boolean) => { this.goods.checked = v; this.onCheck(v) }) Text(this.goods.name).layoutWeight(1) }.padding(12) } }
几个观察:组件用 @ComponentV2 声明(V1/V2 组件可以混用,但状态装饰器不能跨体系配对,@State 不能挂进 @ComponentV2);Repeat 是 V2 体系配套的列表渲染器,键值策略内建;结算条的件数来自 @Computed,没有任何手动同步代码。改勾选任意一项,只有该行与结算条刷新——效果与精心组织的 V1 版本一致,但达成它不再依赖"记得给类加 @Observed、给子组件用 @ObjectLink"的纪律,而是类型系统直接保证。
新项目:直接 V2。它是官方的演进方向,属性级观察与显式数据流能消灭一整类"不刷新"bug;代价是第三方示例与社区文章多为 V1 写法,初期要自己翻译——本教程前两节坚持先讲 V1 也是这个原因:读存量代码与社区资料必须懂 V1。
存量项目:按模块渐进迁移,别整体重写。V1/V2 组件可混用,迁移边界画在"状态痛点多"的模块(深层嵌套对象、派生值同步多的页面),每迁一块跑一遍回归。优先把 @Observed+@ObjectLink 的组合换成 @ObservedV2+@Trace,这一步收益最直接。
混合注意:同一条数据链路保持同一体系。AppStorage 与 V2 的结合要用 V2 配套的观察方式(如通过 @ObservedV2 类包一层),混搭处是迁移 bug 的高发区,值得在代码评审里单列检查项。
⚠️ 常见坑:把 @State 装饰器写进 @ComponentV2,或反过来把 @Local 写进 @Component。编译器会报装饰器不兼容,但报错信息对新手不够友好——记住"V2 组件配 V2 装饰器"这条硬规则即可。
💡 关键直觉:V1 的心智是"框架替你观察整个变量",V2 的心智是"你在类的属性上逐个声明要观察什么"。前者省事但边界模糊,后者多写但精确可控——软件设计里这类取舍反复出现,状态管理只是又一个样本。
本节要点回顾:
状态在内存里的流动至此全部打通。下一章让数据活过重启:先从两行代码的 Preferences 起步,再到完整的关系型数据库建表与增删改查。