一、前言
如果说 ArkTS 的UI布局是应用的骨架,那么状态装饰器就是整个项目的血液。
ArkTS 相较于传统JS/TS最大的升级点,就是响应式数据驱动。开发者无需手动操作DOM,只需修改数据,页面自动刷新。而实现这一切的底层核心,就是各式各样的状态装饰器。
我接触过绝大多数鸿蒙初学者,90%的报错、数据不刷新、父子组件数据错乱,根源全部来自于分不清装饰器:
分不清 @State、@Prop、@Link 三者区别,不知道什么时候用只读、什么时候双向绑定;
多层级组件传参层层嵌套,代码冗余,页面层级越深维护越崩溃;
搞不懂单向数据流与双向数据流,导致父子组件互相篡改数据;
不知道 @Provide/@Consume 跨级通信,无脑一层层props穿透传值;
复杂对象、数组状态不刷新,明明改了数据页面毫无变化。
今天这篇文章,我带你从零拆解 ArkTS 五大高频核心装饰器,理清数据流向、通信范围、读写权限,搭配通俗易懂的案例,彻底根治组件传参疑难杂症,看完直接精通鸿蒙响应式开发。
二、五大装饰器核心总览
先掌握全局定位,根据业务场景直接对号入座,告别盲目乱写:
装饰器 | 作用范围 | 读写权限 | 通信场景 | 通俗总结 |
|---|
@State | 当前组件 | 可读、可写 | 组件内部私有状态 | 自己用,自己改 |
@Prop | 父子组件 | 只读(单向) | 父向子传值,子组件禁止修改 | 父给子,儿子只能看 |
@Link | 父子组件 | 可读、可写(双向) | 父子数据双向同步,一处修改双向刷新 | 父子共用,改哪哪都变 |
@Provide | 跨级组件 | 可读、可写 | 祖先组件向外派发数据 | 高层发布数据 |
@Consume | 跨级组件 | 可读、可写 | 后代组件直接订阅数据 | 底层直接接收,无视层级 |
三、最终实现效果
✅ 透彻理解每一种装饰器底层响应式原理;
✅ 熟练实现组件私有状态、父子单向/双向传参;
✅ 彻底解决多层组件props穿透地狱;
✅ 掌握跨级组件通信,无视页面层级直接共享状态;
✅ 掌握引用类型(对象/数组)刷新规则;
✅ 规避所有装饰器高频报错坑点,适配商业项目开发。
四、逐个拆解+完整实战源码
4.1 @State 组件私有状态
@State 是最基础的状态装饰器,用于定义当前组件内部私有响应式数据。数据仅当前组件及内部子组件可用,外部无法访问。
数据被修改后,组件自动触发局部刷新,是所有响应式的基础。
@Entry@Componentstruct StateDemo { // 定义私有响应式状态 @State count: number = 0; build() { Column({ space: 10 }) { Text(`当前计数:${this.count}`) .fontSize(20) Button("点击累加") .onClick(() => { // 直接修改状态,页面自动刷新 this.count++; }) } .padding(20) }}
4.2 @Prop 父子单向通信(只读)
@Prop 用于接收父组件传递的数据,单向数据流,子组件只读不可写。
父组件数据更新自动同步给子组件;子组件禁止修改数据,从根源避免数据混乱,适合展示类静态数据。
// 子组件@Componentstruct ChildProp { // 接收父组件传值,只读 @Prop title: string; build() { Text(`子组件接收:${this.title}`) .fontSize(16) .backgroundColor("#f5f5f5") .padding(10) }}// 父组件@Entry@Componentstruct PropDemo { @State msg: string = "初始父组件数据"; build() { Column({ space: 20 }) { Text("父组件") .fontSize(20) ChildProp({ title: this.msg }) Button("父组件修改数据") .onClick(() => { this.msg = "数据已被父组件更新"; }) } .padding(20) }}
4.3 @Link 父子双向通信(可读写)
@Link 实现父子组件数据双向绑定,本质是引用传递,父子共用同一份数据源。
无论父组件还是子组件修改数据,双方页面同步刷新。适合弹窗开关、输入框绑定、状态联动等需要双向修改的业务。
// 子组件@Componentstruct ChildLink { // 双向绑定父组件数据 @Link count: number; build() { Column() { Text(`子组件计数:${this.count}`) Button("子组件修改") .onClick(() => this.count--) } }}// 父组件@Entry@Componentstruct LinkDemo { @State num: number = 10; build() { Column({ space: 15 }) { Text(`父组件计数:${this.num}`) // 双向绑定必须加$符号 ChildLink({ count: $num }) Button("父组件修改") .onClick(() => this.num++) } .padding(20) }}
4.4 @Provide + @Consume 跨级全局通信
@Provide(提供者)+ @Consume(消费者),专门解决多层组件穿透传参问题。
祖先组件通过 @Provide 派发数据,后代任意层级组件直接通过 @Consume 订阅,无需一层层手动传递,直接破除层级限制,适合主题、用户信息等全局共享数据。
// 最深层子组件@Componentstruct DeepChild { // 直接订阅祖先组件的数据 @Consume themeColor: string; build() { Text("跨级接收数据") .width(200) .height(80) .backgroundColor(this.themeColor) .textAlign(TextAlign.Center) }}// 中间层组件(无任何传参)@Componentstruct MiddleChild { build() { Column() { DeepChild() } }}// 顶层父组件@Entry@Componentstruct ProvideDemo { // 向外提供全局数据,所有后代均可订阅 @Provide themeColor: string = "#1677ff"; build() { Column({ space: 20 }) { Button("切换主题色") .onClick(() => { this.themeColor = "#f56c6c"; }) MiddleChild() } .padding(20) }}
五、引用类型(对象/数组)刷新规则
很多新手踩坑:基本类型能刷新,对象/数组修改后页面不更新。
5.1 错误写法(直接修改原引用)
ArkTS装饰器监听的是引用地址,直接修改内部属性,引用地址不变,页面不会刷新。
@State user: {name:string,age:number} = {name:"鸿蒙",age:18};// 错误:仅修改属性,引用不变,页面不刷新this.user.age = 20;
5.2 正确写法(覆盖引用)
通过解构赋值、对象重新创建,更换引用地址,触发响应式刷新。
// 正确:更换引用地址,触发页面刷新this.user = {...this.user,age:20};
六、生产环境避坑指南
禁止滥用@Link:非必要不要双向绑定,优先使用@Prop单向数据流,降低数据维护难度;
@Link传参必须带$:父组件向子组件传递Link变量,必须添加$符号,缺失直接编译报错;
私有状态只用@State:组件内部临时变量不要用Provide,避免全局状态污染;
跨级通信优先Provide:三层及以上组件传参,直接抛弃Props穿透,使用Provide/Consume;
数组不要直接push:push不会更新引用,建议使用解构赋值:list = [...list, newItem];
默认值优先级:@Prop支持设置默认值,外部未传参时自动启用默认值,防止undefined报错。