4.2 @Prop @Link @Provide 组件通信


本节摘要:组件树里的状态传递有四种主力装饰器:@Prop 单向同步、@Link 双向同步、@Provide/@Consume 跨层共享、@Observed+@ObjectLink 嵌套对象联动。本节用购物车场景把四种写法各跑一遍,给出选择判据。

把状态传下去的四种方式

第 4.1 节的状态都住在页面里。一旦界面拆成组件,数据就得流动。先给结论表,再逐个验证:

装饰器 方向 数据特征 典型场景
@Prop 父到子单向 本地副本,子改不影响父 展示型子组件接收标题、标签
@Link 父子双向 引用同步,双向联动 开关、计数器、表单控件
@Provide + @Consume 跨层任意 祖先提供,后代消费 全局登录态、主题色
@Observed + @ObjectLink 嵌套对象 对象级精确刷新 列表项对象内部字段变化

场景驱动:一个购物车页面。页面持有商品列表与总数量;列表项是子组件,可勾选;顶部还有一个跨了几层嵌套的结算条组件显示已选数量。这个场景恰好把四种通行证全部用上。

@Prop:给展示组件喂只读数据。 列表项的商品名、价格是纯展示:

@Component struct PriceTag { @Prop price: number = 0 build() { Text(`¥${this.price.toFixed(2)}`).fontSize(16) } }

@Prop 生成副本,父组件更新时同步给子,子组件内部修改只改自己的副本,不会回传。展示型组件一律优先 @Prop——数据流向单一,出问题好查。

@Link:勾选状态双向联动。 每个列表项有勾选态,页面需要感知它来计算总价:

@Component struct CartItem { @Link checked: boolean build() { Toggle({ type: ToggleType.Checkbox, isOn: this.checked }) .onChange((v: boolean) => { this.checked = v }) } } // 页面里使用:CartItem({ checked: this.itemChecked[i] })

@Link 是引用同步:父里改、子刷新;子里改、父也刷新。传参时父侧不加 的写法在新版本已兼容,但读旧代码时见到 value 前缀要知道那是 @Link 的老式传参语法,指传引用而非拷贝。

@Provide/@Consume:结算条跨层取数。 结算条埋在很深的布局嵌套里,把已选数量一层层透传要穿过三四个中间组件——中间组件被迫接收并转发与自己无关的数据,这叫"prop 钻透",是组件设计的坏味道。跨层共享的解法:

@Entry @Component struct CartPage { @Provide('selectedCount') selectedCount: number = 0 build() { Column() { this.cartList() SettlementBar() } } } @Component struct SettlementBar { @Consume('selectedCount') selectedCount: number build() { Text(`已选 ${this.selectedCount} 件,去结算`).fontSize(18) } }

祖先 @Provide 声明,任意层级的后代 @Consume 消费,同名匹配。代价是数据来源不再显式(看 SettlementBar 的代码不知道数据从哪来),所以判据是:跨三层以上且语义上是"全局/区域共享"的状态才用,两层以内老老实实传参

@Observed + @ObjectLink:列表项对象内部联动。 商品对象有 name、price、checked 多个字段,列表项组件接收整个对象并要对其内部字段变化做精确刷新:

@Observed class Goods { name: string price: number checked: boolean constructor(name: string, price: number) { this.name = name this.price = price this.checked = false } } @Component struct GoodsRow { @ObjectLink goods: Goods build() { Row({ space: 12 }) { Toggle({ type: ToggleType.Checkbox, isOn: this.goods.checked }) .onChange((v: boolean) => { this.goods.checked = v }) Text(this.goods.name).layoutWeight(1) PriceTag({ price: this.goods.price }) }.padding(12) } }

@Observed 标记类,@ObjectLink 接收实例,goods.checked 的修改就能触发 GoodsRow 精确刷新——这正好补上了 4.1 节"嵌套对象内部变化不可见"的缺口,且刷新范围只有这一行,列表其他项纹丝不动。

应用级:AppStorage 与环境

再往外一层是应用级共享。AppStorage 是应用内存里的全局键值仓库,任何组件通过链接接口与某个键绑定:

// 登录成功后写入 AppStorage.setOrCreate('userName', '小舟'); AppStorage.setOrCreate('isVip', true); // 任意组件里双向链接 @StorageLink('isVip') isVip: boolean = false @StorageProp('userName') userName: string = ''

@StorageLink 双向、@StorageProp 单向,语义与 @Link/@Prop 平行。购物车的"用户是否会员"影响价格显示,这类跨页面共享的登录态、会员态就放 AppStorage。与它相邻的两个工具:PersistentStorage 把指定键自动落盘(重启恢复,底层是轻量 preferences,第 5 章详讲存储侧),Environment 提供只读的设备环境(语言、深浅色)。应用级状态的纪律与 @Provide 相同:真正全局的才上,且键名集中定义,别散落魔法字符串

演练:拼装完整购物车

把上面碎片拼起来。背景:商品 20 件,列表可勾选,结算条实时显示已选件数与总价,会员价在总价上打九折。操作:Goods 类加 @Observed;页面 @State goodsList 数组 + @Provide selectedCount;GoodsRow 用 @ObjectLink 接对象,勾选时改 goods.checked 并回调整 selectedCount;结算条 @Consume 件数、@StorageLink 读 isVip;PriceTag 用 @Prop。结果:勾选任意商品,只有该行与结算条刷新,件数、价格(含会员折扣)全部实时正确。解读:五种装饰器各司其职,数据流向可以画成一张单向图——对象状态在 Goods 实例里,跨组件信号沿 Provide/Storage 走,展示副本经 Prop 下发。变式:加"全选"开关(页面级遍历设置所有 goods.checked),观察 @ObjectLink 如何让每行各自刷新——这就是选对装饰器的复利。

⚠️ 常见坑:@Consume 与 @Provide 的名字不匹配时,运行才报错(找不到提供者),编译期无提示。名字统一用常量定义可以从根上杜绝。

💡 关键直觉:装饰器的选择本质是回答"谁拥有数据、谁有权修改"。展示的用 Prop、联动的用 Link、共享的用 Provide/Storage、嵌套的用 ObjectLink——先回答权属问题,装饰器自然就选出来了。

本节要点回顾:

  • 四种传递:@Prop 单向副本、@Link 双向引用、@Provide/@Consume 跨层、@Observed+@ObjectLink 嵌套对象。
  • 判据是数据权属:先定谁拥有谁可改,再选装饰器。
  • prop 钻透是坏味道:跨三层以上的全局语义状态才用跨层共享。
  • AppStorage 家族:应用级键值 + 双向/单向链接 + PersistentStorage 落盘 + Environment 环境。
  • 键名纪律:跨层共享的名字集中定义,避免魔法字符串。

这些机制都属于"V1 体系"。HarmonyOS 还有一套 V2 状态管理,观察粒度与写法都不同。下一节正面回答:新项目该用哪套。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U