以下针对父传子 @Prop、父子双向 @Link、跨层 @Provide+@Consume、全局 @AppStorage、持久化 @StorageLink、监听 @Watch 6 个高频装饰器,从核心特性、数据流向、使用规则、实战代码四方面拆解,案例均基于鸿蒙Stage 模型 + ArkTS4.x,可直接复用。

所有案例遵循鸿蒙组件通信基本原则:子组件不可直接修改父组件的原始数据,装饰器本质是做数据同步 / 映射,双向通信由框架底层实现,避免手动传参的繁琐。

一、@Prop:父→子 单向传递(子只读,父独控)

核心特性

  1. 数据流向父组件 @State / 全局状态 → 子组件 @Prop,单向只读,子组件无法修改 @Prop 变量(修改会报错);
  2. 数据同步:父组件数据更新,子组件 @Prop 会自动同步并触发子组件重渲染;
  3. 初始化要求:子组件 @Prop不能自己初始化,必须由父组件传值赋值;
  4. 适用类型:支持基础类型(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:父↔子 双向同步(父子互改,数据互通)

核心特性

  1. 数据流向父组件 @State / 全局状态 ↔ 子组件 @Link,双向同步,父子修改各自变量,对方会实时同步并触发重渲染;
  2. 初始化要求:子组件 @Link不能自己初始化,必须由父组件通过 **符号传值(变量名 `,表示传递数据的引用);
  3. 适用类型:支持基础类型、对象、数组(引用类型双向同步更高效);
  4. 底层逻辑:@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 直接传变量名即可)。

效果

  1. 父点「打开弹窗」,子组件显示;子点「关闭弹窗」,父组件modalState同步为 false,子组件隐藏;
  2. 父点「计数 + 2」,子组件计数同步增加;子点「计数 + 1」,父组件计数同步增加,实现双向互通。

三、@Provide + @Consume:祖先→后代 跨多层传递(无需逐层透传)

核心特性

  1. 数据流向祖先组件 @Provide 提供数据 → 任意层级后代组件 @Consume 消费数据,支持双向同步(后代修改 @Consume,祖先 @Provide 会同步更新);
  2. 匹配规则:通过变量名 / 别名匹配,而非组件传参,无需像 @Prop/@Link 那样逐层传递(解决「透传地狱」问题,比如爷→父→子,父组件无需做任何处理);
  3. 初始化要求:@Provide 在祖先组件初始化,@Consume 在后代组件仅声明,不初始化
  4. 适用场景:跨 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)
  }
}

效果

  1. 孙组件直接渲染根组件 @Provide 的userInfo,中间的子组件无需做任何传参操作,解决透传问题;
  2. 孙组件点击「修改用户名」,根组件的userInfo.name同步更新,页面文字实时变化,实现跨层双向同步。

扩展:别名匹配(避免变量名冲突)

如果多个 @Provide 变量名重复,可通过别名匹配,语法:

// 祖先组件:@Provide("别名") 变量名
@Provide("globalUser") userInfo = { name: "张三" };

// 后代组件:@Consume("别名") 变量名(变量名可自定义)
@Consume("globalUser") currentUser;

四、@AppStorage:全局应用状态共享(所有页面 / 组件互通)

核心特性

  1. 作用域整个应用全局,所有页面、组件均可访问和修改,修改后全应用同步更新并触发重渲染;
  2. 存储位置内存中,应用退出后数据丢失(非持久化);
  3. 初始化要求:可在任意组件通过@AppStorage("key")声明,通过key 值全局匹配,无需提前初始化(未赋值时为 undefined);
  4. 底层逻辑:基于鸿蒙的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)
  }
}

效果

  1. 页面 A 点击「+1」,全局计数更新,跳转到页面 B 后,页面 B 的计数与页面 A 保持一致;
  2. 页面 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:全局持久化状态(应用退出,数据仍在)

核心特性

  1. 作用域整个应用全局,与 @AppStorage 一致,所有页面 / 组件可双向同步;
  2. 存储位置手机本地持久化存储(鸿蒙的 Preferences),应用退出、手机重启后数据不会丢失
  3. 初始化要求:通过@StorageLink("key")声明,key 值全局唯一,首次赋值时自动持久化到本地;
  4. 性能注意:持久化操作有轻微性能开销,不适合高频修改的变量(如定时器计数),适合低频修改的全局配置。

核心使用场景

应用需要持久化的全局状态,比如用户登录状态(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)
  }
}

效果

  1. 点击按钮切换夜间模式,nightMode被 @StorageLink 自动持久化到手机本地;
  2. 关闭应用并重新打开,nightMode会自动从本地读取,保留上次的模式设置,实现持久化效果。

@AppStorage vs @StorageLink 核心区别

装饰器 存储位置 持久化 应用退出后数据 适用场景
@AppStorage 内存 丢失 全局临时状态
@StorageLink 本地存储 保留 全局持久化状态

六、@Watch:监听状态变量变化,触发自定义逻辑

核心特性

  1. 作用:给 **@State/@Link/@Provide/@Consume/@AppStorage/@StorageLink** 等响应式状态添加监听器,当变量值发生变化时,自动执行指定的自定义函数;
  2. 监听规则:仅监听变量值的实际变化(值不变时,多次赋值不会触发监听);
  3. 使用语法@Watch("监听函数名") + 响应式装饰器,监听函数在组件内定义,接收新值、旧值两个参数(可选);
  4. 适用场景:数据变化后需要执行额外逻辑,比如搜索防抖、数据校验、联动更新其他变量、请求接口等。

核心使用场景

  1. 搜索框输入内容,监听输入变化实现防抖请求(输入停止后再请求接口);
  2. 表单字段变化,实时校验字段合法性(如手机号、密码格式);
  3. 全局状态变化,联动更新页面其他状态(如夜间模式变化,同步修改所有组件的颜色);
  4. 计数达到指定值时,执行弹窗提示 / 业务逻辑

实战案例 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 避坑要点

  1. 仅监听响应式状态:@Watch 只能配合响应式装饰器使用,不能监听 @Local(非响应式)变量;
  2. 避免循环触发:监听函数中不要修改当前监听的变量,否则会造成「变量变化→触发监听→修改变量→再次触发监听」的死循环;
  3. 及时清理定时器:如果监听函数中有定时器 / 订阅等,需在组件onDestroy生命周期中清理,避免内存泄漏。

七、6 个装饰器核心总结(速查版)

装饰器组合 / 单个 数据流向 作用域 核心特性 典型场景
@Prop 父→子 单向 父子组件 子只读,父修改子同步 传递标题、配置、静态数据
@Link 父↔子 双向 父子组件 父子互改,需 $ 传引用 弹窗控制、表单输入、计数
@Provide+@Consume 祖先→后代 跨多层 双向 跨多层组件 按 key 匹配,无需逐层透传 全局用户信息、主题色
@AppStorage 全应用 双向 整个应用 内存存储,应用退出数据丢失 全局临时状态、加载状态
@StorageLink 全应用 双向 整个应用 本地持久化,应用退出数据保留 用户登录状态、个性化配置
@Watch 监听状态变化 组件内 触发自定义逻辑,支持防抖 / 校验 搜索防抖、数据校验、联动更新

八、装饰器使用优先级原则(开发必遵)

  1. 组件内单独使用:用@State(响应式)/@Local(非响应式);
  2. 父子单层通信:简单展示用@Prop,双向操作用@Link
  3. 跨多层组件通信:直接用@Provide+@Consume,拒绝逐层透传;
  4. 多页面全局通信:临时状态用@AppStorage,持久化用@StorageLink
  5. 数据变化需执行额外逻辑:给响应式装饰器加@Watch

遵循以上原则,可高效实现鸿蒙 ArkTS 的组件通信和状态管理,避免冗余代码和性能问题。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐