ArkTS 核心通信 / 状态装饰器详解(附实战案例)
作用:给 **@State/@Link/@Provide/@Consume/@AppStorage/@StorageLink** 等响应式状态添加监听器,当变量值发生变化时,自动执行指定的自定义函数;监听规则:仅监听变量值的实际变化(值不变时,多次赋值不会触发监听);使用语法@Watch("监听函数名") + 响应式装饰器,监听函数在组件内定义,接收新值、旧值两个参数(可选);适用场景:数据变化后
以下针对父传子 @Prop、父子双向 @Link、跨层 @Provide+@Consume、全局 @AppStorage、持久化 @StorageLink、监听 @Watch 6 个高频装饰器,从核心特性、数据流向、使用规则、实战代码四方面拆解,案例均基于鸿蒙Stage 模型 + ArkTS4.x,可直接复用。
所有案例遵循鸿蒙组件通信基本原则:子组件不可直接修改父组件的原始数据,装饰器本质是做数据同步 / 映射,双向通信由框架底层实现,避免手动传参的繁琐。
一、@Prop:父→子 单向传递(子只读,父独控)
核心特性
- 数据流向:父组件 @State / 全局状态 → 子组件 @Prop,单向只读,子组件无法修改 @Prop 变量(修改会报错);
- 数据同步:父组件数据更新,子组件 @Prop 会自动同步并触发子组件重渲染;
- 初始化要求:子组件 @Prop不能自己初始化,必须由父组件传值赋值;
- 适用类型:支持基础类型(number/string/boolean)、简单对象 / 数组(注意:对象 / 数组为引用传递,子组件不可修改引用,可修改内部属性)。
核心使用场景
父组件向子组件传递静态配置、展示型数据、不可变参数,比如标题文字、按钮禁用状态、列表渲染的单条数据、主题色值等。
实战案例:父组件传标题给子组件
// 子组件:ChildProp.ets
@Component
export struct ChildProp {
// 子组件@Prop:仅接收,不初始化,只读
@Prop title: string;
@Prop isDisabled: boolean;
build() {
Column({ space: 20 }) {
// 渲染父组件传递的标题
Text(`子组件接收的标题:${this.title}`)
.fontSize(20);
// 渲染父组件传递的按钮状态
Button("子组件按钮(父控禁用)")
.disabled(this.isDisabled)
.backgroundColor(this.isDisabled ? Color.Grey : Color.Blue);
// 错误演示:子组件直接修改@Prop会编译报错
// Button("尝试修改@Prop").onClick(() => { this.title = "修改"; })
}
}
}
// 父组件:ParentProp.ets
@Entry
@Component
struct ParentProp {
// 父组件@State:作为数据源
@State parentTitle: string = "父组件初始标题";
@State parentBtnState: boolean = true;
build() {
Column({ space: 30 }) {
// 父组件自身渲染
Text(`父组件标题:${this.parentTitle}`)
.fontSize(24);
// 父组件修改自身状态,子组件@Prop自动同步
Button("父组件修改标题+启用子按钮")
.onClick(() => {
this.parentTitle = "父组件修改后的标题";
this.parentBtnState = false;
});
// 父组件向子组件传值:绑定@State到子组件@Prop
ChildProp({
title: this.parentTitle,
isDisabled: this.parentBtnState
})
}
.padding(30)
}
}
效果
点击父组件按钮,父组件parentTitle/parentBtnState更新,子组件 @Prop 自动同步,标题文字和按钮禁用状态实时变化;子组件尝试修改 @Prop 会直接编译报错,保证数据单向可控。
二、@Link:父↔子 双向同步(父子互改,数据互通)
核心特性
- 数据流向:父组件 @State / 全局状态 ↔ 子组件 @Link,双向同步,父子修改各自变量,对方会实时同步并触发重渲染;
- 初始化要求:子组件 @Link不能自己初始化,必须由父组件通过 **符号传值(变量名 `,表示传递数据的引用);
- 适用类型:支持基础类型、对象、数组(引用类型双向同步更高效);
- 底层逻辑:@Link 并非复制数据,而是绑定父组件状态的引用,因此父子修改的是「同一个数据源」。
核心使用场景
父子组件需要共同操作同一个状态,比如弹窗的显示 / 隐藏、表单输入框内容、开关状态、计数累加等。
实战案例:父子双向控制弹窗 + 修改计数
// 子组件:ChildLink.ets
@Component
export struct ChildLink {
// 子组件@Link:双向同步,由父组件$传值
@Link isShowModal: boolean;
@Link count: number;
build() {
Column({ space: 20 }) {
// 子组件修改@Link,父组件同步更新
Button("子组件关闭弹窗")
.onClick(() => { this.isShowModal = false; });
Button("子组件计数+1")
.onClick(() => { this.count++; });
Text(`子组件计数:${this.count}`)
.fontSize(20);
}
}
}
// 父组件:ParentLink.ets
@Entry
@Component
struct ParentLink {
// 父组件@State:数据源
@State modalState: boolean = false;
@State parentCount: number = 0;
build() {
Column({ space: 30 }) {
Text(`父组件计数:${this.parentCount}`)
.fontSize(24);
// 父组件修改状态,子组件@Link同步更新
Button("父组件打开弹窗")
.onClick(() => { this.modalState = true; });
Button("父组件计数+2")
.onClick(() => { this.parentCount += 2; });
// 父组件传值给@Link:必须加$符号(传递引用)
if (this.modalState) {
ChildLink({
isShowModal: $modalState,
count: $parentCount
})
.padding(20)
.backgroundColor(Color.Pink);
}
}
.padding(30)
}
}
关键要点
父组件传 @Link 时 ** 必须加,比如modalState`,如果漏写 $ 会编译报错,这是 @Link 和 @Prop 的核心区别(@Prop 直接传变量名即可)。
效果
- 父点「打开弹窗」,子组件显示;子点「关闭弹窗」,父组件
modalState同步为 false,子组件隐藏; - 父点「计数 + 2」,子组件计数同步增加;子点「计数 + 1」,父组件计数同步增加,实现双向互通。
三、@Provide + @Consume:祖先→后代 跨多层传递(无需逐层透传)
核心特性
- 数据流向:祖先组件 @Provide 提供数据 → 任意层级后代组件 @Consume 消费数据,支持双向同步(后代修改 @Consume,祖先 @Provide 会同步更新);
- 匹配规则:通过变量名 / 别名匹配,而非组件传参,无需像 @Prop/@Link 那样逐层传递(解决「透传地狱」问题,比如爷→父→子,父组件无需做任何处理);
- 初始化要求:@Provide 在祖先组件初始化,@Consume 在后代组件仅声明,不初始化;
- 适用场景:跨 2 层及以上的组件通信,比如 APP 全局主题、用户登录信息、页面通用配置等。
核心使用场景
多层组件嵌套时,祖先组件向深层后代组件传递数据,避免「爷传父、父传子」的繁琐透传,比如根组件(@Provide)→ 页面组件 → 卡片组件 → 按钮组件(@Consume),中间的页面 / 卡片组件无需参与传参。
实战案例:根组件提供用户信息,孙组件直接消费(爷→父→孙,跨 2 层)
// 孙组件:GrandChildConsume.ets
@Component
export struct GrandChildConsume {
// 孙组件@Consume:匹配祖先@Provide的变量名,无需初始化,双向同步
@Consume userInfo: { name: string; age: number; isLogin: boolean };
build() {
Column({ space: 15 }) {
Text(`孙组件消费用户信息:`)
.fontSize(18)
.fontWeight(FontWeight.Bold);
Text(`姓名:${this.userInfo.name}`)
Text(`年龄:${this.userInfo.age}`)
Text(`登录状态:${this.userInfo.isLogin ? "已登录" : "未登录"}`);
// 孙组件修改@Consume,祖先@Provide同步更新(双向)
Button("孙组件修改用户名")
.onClick(() => { this.userInfo.name = "鸿蒙开发"; });
}
.padding(20)
.backgroundColor(Color.LightBlue);
}
}
// 子组件:ChildProvide.ets
@Component
export struct ChildProvide {
// 子组件:仅做嵌套,无需传参,完全不感知userInfo
build() {
Column({ space: 20 }) {
Text("子组件(仅嵌套,无传参)")
.fontSize(20);
// 直接渲染孙组件,孙组件可直接消费根组件的@Provide
GrandChildConsume();
}
}
}
// 根组件(祖先):ParentProvide.ets
@Entry
@Component
struct ParentProvide {
// 根组件@Provide:提供数据,初始化赋值
@Provide userInfo: { name: string; age: number; isLogin: boolean } = {
name: "张三",
age: 25,
isLogin: true
};
build() {
Column({ space: 30 }) {
Text(`根组件用户信息:${this.userInfo.name}`)
.fontSize(24);
// 根组件渲染子组件,无需传参userInfo
ChildProvide();
}
.padding(30)
}
}
效果
- 孙组件直接渲染根组件 @Provide 的
userInfo,中间的子组件无需做任何传参操作,解决透传问题; - 孙组件点击「修改用户名」,根组件的
userInfo.name同步更新,页面文字实时变化,实现跨层双向同步。
扩展:别名匹配(避免变量名冲突)
如果多个 @Provide 变量名重复,可通过别名匹配,语法:
// 祖先组件:@Provide("别名") 变量名
@Provide("globalUser") userInfo = { name: "张三" };
// 后代组件:@Consume("别名") 变量名(变量名可自定义)
@Consume("globalUser") currentUser;
四、@AppStorage:全局应用状态共享(所有页面 / 组件互通)
核心特性
- 作用域:整个应用全局,所有页面、组件均可访问和修改,修改后全应用同步更新并触发重渲染;
- 存储位置:内存中,应用退出后数据丢失(非持久化);
- 初始化要求:可在任意组件通过
@AppStorage("key")声明,通过key 值全局匹配,无需提前初始化(未赋值时为 undefined); - 底层逻辑:基于鸿蒙的AppStorage 全局状态池,所有 @AppStorage 变量都是对池内 key 的引用,本质是「单例数据源」。
核心使用场景
应用全局共享的临时状态,比如全局加载状态、页面主题色、临时登录 token、全局搜索关键词等,无需手动通过组件传参,所有页面直接取用。
实战案例:两个独立页面共享全局计数(页面 A 修改,页面 B 同步)
页面 A:PageA.ets(修改全局计数)
// 页面A:修改@AppStorage全局状态
@Entry
@Component
struct PageA {
// 声明全局状态:key为"globalCount",默认值0(首次赋值初始化全局池)
@AppStorage("globalCount") globalCount: number = 0;
build() {
Column({ space: 30 }) {
Text(`页面A - 全局计数:${this.globalCount}`)
.fontSize(24);
// 页面A修改全局计数,全应用同步
Button("页面A:全局计数+1")
.onClick(() => { this.globalCount++; });
// 跳转到页面B
Navigator({ target: "pages/PageB" }) {
Text("跳转到页面B")
.fontSize(18)
.color(Color.Blue);
}
}
.padding(30)
}
}
页面 B:PageB.ets(消费并修改全局计数)
// 页面B:消费并修改@AppStorage全局状态
@Entry
@Component
struct PageB {
// 声明全局状态:匹配key"globalCount",无需重新初始化,直接取用全局池数据
@AppStorage("globalCount") globalCount: number = 0;
build() {
Column({ space: 30 }) {
Text(`页面B - 全局计数:${this.globalCount}`)
.fontSize(24);
// 页面B修改全局计数,页面A也会同步
Button("页面B:全局计数+2")
.onClick(() => { this.globalCount += 2; });
// 跳回页面A
Navigator({ target: "pages/PageA" }) {
Text("跳回页面A")
.fontSize(18)
.color(Color.Blue);
}
}
.padding(30)
}
}
效果
- 页面 A 点击「+1」,全局计数更新,跳转到页面 B 后,页面 B 的计数与页面 A 保持一致;
- 页面 B 点击「+2」,全局计数再次更新,跳回页面 A 后,页面 A 的计数同步更新,实现两个独立页面的全局数据互通。
手动操作 AppStorage 全局池
除了 @AppStorage 装饰器,还可通过AppStorage.set/get/delete手动操作全局池,适合在非组件代码(如工具类)中使用:
import { AppStorage } from '@ohos.data.AppStorage';
// 手动设置全局数据
AppStorage.set("globalCount", 10);
// 手动获取全局数据
const count = AppStorage.get("globalCount");
// 手动删除全局数据
AppStorage.delete("globalCount");
五、@StorageLink:全局持久化状态(应用退出,数据仍在)
核心特性
- 作用域:整个应用全局,与 @AppStorage 一致,所有页面 / 组件可双向同步;
- 存储位置:手机本地持久化存储(鸿蒙的 Preferences),应用退出、手机重启后数据不会丢失;
- 初始化要求:通过
@StorageLink("key")声明,key 值全局唯一,首次赋值时自动持久化到本地; - 性能注意:持久化操作有轻微性能开销,不适合高频修改的变量(如定时器计数),适合低频修改的全局配置。
核心使用场景
应用需要持久化的全局状态,比如用户登录状态(isLogin)、用户 ID、个性化配置(如夜间模式、字体大小)、上次访问页面等,实现「应用重启后数据不丢失」。
实战案例:持久化保存用户夜间模式状态(应用重启后仍保留)
// 页面:StorageLinkDemo.ets
@Entry
@Component
struct StorageLinkDemo {
// 全局持久化状态:key为"nightMode",默认值false(首次赋值持久化到本地)
@StorageLink("nightMode") nightMode: boolean = false;
// 页面背景色,根据夜间模式动态变化
@State bgColor: Color = this.nightMode ? Color.Black : Color.White;
@State textColor: Color = this.nightMode ? Color.White : Color.Black;
build() {
Column({ space: 30 }) {
Text(`当前模式:${this.nightMode ? "夜间模式" : "日间模式"}`)
.fontSize(24)
.fontColor(this.textColor);
// 切换夜间模式,@StorageLink自动持久化到本地
Button("切换夜间/日间模式")
.fontColor(this.textColor)
.backgroundColor(this.nightMode ? Color.Grey : Color.Blue)
.onClick(() => {
this.nightMode = !this.nightMode;
this.bgColor = this.nightMode ? Color.Black : Color.White;
this.textColor = this.nightMode ? Color.White : Color.Black;
});
}
.width('100%')
.height('100%')
.backgroundColor(this.bgColor)
.padding(30)
}
}
效果
- 点击按钮切换夜间模式,
nightMode被 @StorageLink 自动持久化到手机本地; - 关闭应用并重新打开,
nightMode会自动从本地读取,保留上次的模式设置,实现持久化效果。
@AppStorage vs @StorageLink 核心区别
| 装饰器 | 存储位置 | 持久化 | 应用退出后数据 | 适用场景 |
|---|---|---|---|---|
| @AppStorage | 内存 | 否 | 丢失 | 全局临时状态 |
| @StorageLink | 本地存储 | 是 | 保留 | 全局持久化状态 |
六、@Watch:监听状态变量变化,触发自定义逻辑
核心特性
- 作用:给 **@State/@Link/@Provide/@Consume/@AppStorage/@StorageLink** 等响应式状态添加监听器,当变量值发生变化时,自动执行指定的自定义函数;
- 监听规则:仅监听变量值的实际变化(值不变时,多次赋值不会触发监听);
- 使用语法:
@Watch("监听函数名") + 响应式装饰器,监听函数在组件内定义,接收新值、旧值两个参数(可选); - 适用场景:数据变化后需要执行额外逻辑,比如搜索防抖、数据校验、联动更新其他变量、请求接口等。
核心使用场景
- 搜索框输入内容,监听输入变化实现防抖请求(输入停止后再请求接口);
- 表单字段变化,实时校验字段合法性(如手机号、密码格式);
- 全局状态变化,联动更新页面其他状态(如夜间模式变化,同步修改所有组件的颜色);
- 计数达到指定值时,执行弹窗提示 / 业务逻辑。
实战案例 1:基础监听 —— 计数变化,触发弹窗提示
// 基础监听:@State + @Watch
@Entry
@Component
struct WatchBasicDemo {
// 给@State添加@Watch,监听函数为onCountChange
@Watch("onCountChange")
@State count: number = 0;
build() {
Column({ space: 30 }) {
Text(`当前计数:${this.count}`)
.fontSize(24);
Button("计数+1")
.onClick(() => { this.count++; });
}
.padding(30)
}
// 监听函数:参数为「新值、旧值」(可选)
onCountChange(newVal: number, oldVal: number) {
console.log(`计数从${oldVal}变为${newVal}`);
// 计数达到5时,弹出提示
if (newVal === 5) {
AlertDialog.show({
title: "提示",
message: `计数达到${newVal}啦!`,
confirm: { text: "确定" }
});
}
}
}
实战案例 2:实战高频 —— 搜索框防抖(输入停止 1 秒后请求接口)
// 实战防抖:@State + @Watch 实现搜索防抖
@Entry
@Component
struct WatchSearchDebounce {
// 监听搜索输入变化,触发onSearchChange
@Watch("onSearchChange")
@State searchKey: string = "";
// 防抖定时器
private timer: number | null = null;
build() {
Column({ space: 30 }) {
// 搜索输入框,绑定@State
Input({ placeholder: "请输入搜索关键词", value: this.searchKey })
.onChange((value) => { this.searchKey = value; })
.fontSize(18)
.width('80%')
.padding(10)
.border({ width: 1, color: Color.Grey });
Text(`当前搜索关键词:${this.searchKey || "无"}`)
.fontSize(20);
}
.padding(30)
}
// 搜索监听函数:实现防抖逻辑
onSearchChange(newVal: string) {
// 清除上一次的定时器
if (this.timer) {
clearTimeout(this.timer);
}
// 输入为空,直接返回
if (!newVal.trim()) {
console.log("搜索关键词为空,无需请求");
return;
}
// 1秒后执行接口请求
this.timer = setTimeout(() => {
this.requestSearchApi(newVal);
this.timer = null;
}, 1000);
}
// 模拟搜索接口请求
requestSearchApi(key: string) {
console.log(`开始请求搜索接口,关键词:${key}`);
// 实际开发中替换为真实的网络请求
}
}
效果
输入框连续输入时,不会频繁触发接口请求,输入停止 1 秒后才会执行requestSearchApi,有效减少接口请求次数,提升性能(前端开发必备防抖逻辑)。
@Watch 避坑要点
- 仅监听响应式状态:@Watch 只能配合响应式装饰器使用,不能监听 @Local(非响应式)变量;
- 避免循环触发:监听函数中不要修改当前监听的变量,否则会造成「变量变化→触发监听→修改变量→再次触发监听」的死循环;
- 及时清理定时器:如果监听函数中有定时器 / 订阅等,需在组件onDestroy生命周期中清理,避免内存泄漏。
七、6 个装饰器核心总结(速查版)
| 装饰器组合 / 单个 | 数据流向 | 作用域 | 核心特性 | 典型场景 |
|---|---|---|---|---|
| @Prop | 父→子 单向 | 父子组件 | 子只读,父修改子同步 | 传递标题、配置、静态数据 |
| @Link | 父↔子 双向 | 父子组件 | 父子互改,需 $ 传引用 | 弹窗控制、表单输入、计数 |
| @Provide+@Consume | 祖先→后代 跨多层 双向 | 跨多层组件 | 按 key 匹配,无需逐层透传 | 全局用户信息、主题色 |
| @AppStorage | 全应用 双向 | 整个应用 | 内存存储,应用退出数据丢失 | 全局临时状态、加载状态 |
| @StorageLink | 全应用 双向 | 整个应用 | 本地持久化,应用退出数据保留 | 用户登录状态、个性化配置 |
| @Watch | 监听状态变化 | 组件内 | 触发自定义逻辑,支持防抖 / 校验 | 搜索防抖、数据校验、联动更新 |
八、装饰器使用优先级原则(开发必遵)
- 组件内单独使用:用
@State(响应式)/@Local(非响应式); - 父子单层通信:简单展示用
@Prop,双向操作用@Link; - 跨多层组件通信:直接用
@Provide+@Consume,拒绝逐层透传; - 多页面全局通信:临时状态用
@AppStorage,持久化用@StorageLink; - 数据变化需执行额外逻辑:给响应式装饰器加
@Watch。
遵循以上原则,可高效实现鸿蒙 ArkTS 的组件通信和状态管理,避免冗余代码和性能问题。
更多推荐
所有评论(0)