方案对比

方案 核心机制 数据流向 优点 缺点 推荐场景
1. @Ref 直接调用 获取子组件实例,直接调用其方法 父 → 子 简单直接,精准调用 破坏组件封装性,耦合度最高 需要主动触发子组件行为(如表单验证、动画播放等)
2. @Link 双向同步 父组件状态变量与子组件状态变量双向绑定 父 ↔ 子 数据驱动,子组件状态变化可自动同步回父组件 需同时修改父子组件,适用于状态同步场景 子组件需要修改父组件数据并实时响应
3. Controller 代理模式 通过自定义控制器类封装子组件方法 父 → 子 逻辑集中,便于管理多个组件,复用性强 代码量较多,需要额外定义控制器类 需要统一管理多个子组件或提供对外API的复杂组件
4. emitter 事件总线 通过全局事件总线发布/订阅事件 任意方向 跨组件、跨页面通信能力强,解耦 事件管理复杂,需手动清理监听,有内存泄漏风险 非直系组件,或需要跨越多层组件通信的场景

详细说明与示例

1. @Ref 直接调用(最推荐)

这是实现“父调子”最标准、最直接的方式。通过在父组件中为子组件添加 @Ref 装饰器,你可以获得子组件的实例引用,从而调用其内部定义的任何公共方法。

子组件 (Child.ets)

typescript

@Component
export struct ChildComponent {
  // 定义一个公共方法,供父组件调用
  public childMethod() {
    console.log('子组件的方法被父组件调用了');
    // 在这里执行子组件的业务逻辑,例如更新UI、重置表单等
  }

  build() {
    Column() {
      Text('我是子组件')
    }
  }
}

父组件 (Parent.ets)

typescript

@Entry
@Component
struct ParentComponent {
  // 1. 定义一个 Ref 对象,泛型为子组件类型
  childRef: Ref<ChildComponent> = Ref();

  // 2. 父组件的方法,调用子组件的方法
  callChildMethod() {
    // 通过 value? 安全地调用子组件的方法
    this.childRef.value?.childMethod();
  }

  build() {
    Column() {
      // 3. 为子组件添加 ref 属性,绑定到定义的 Ref 对象
      ChildComponent().ref(this.childRef)

      Button('父组件调用子组件方法')
        .onClick(() => {
          this.callChildMethod();
        })
    }
  }
}
2. @Link 数据双向同步

这种方式不是直接“调用”方法,而是通过改变共享数据,利用数据驱动UI的特性,“触发”子组件响应。当父组件修改了一个用 @Link 与子组件绑定的变量时,子组件会监听到变化并自动执行相应的逻辑。

子组件 (Child.ets)

typescript

@Component
export struct ChildComponent {
  // 使用 @Link 装饰器,与父组件的 @State 变量建立双向绑定
  @Link isActive: boolean;

  // 监听 isActive 的变化
  aboutToUpdate() {
    if (this.isActive) {
      console.log('isActive 变为 true,执行激活逻辑');
      // 在这里执行激活时需要做的操作
    } else {
      console.log('isActive 变为 false,执行非激活逻辑');
    }
  }

  build() {
    Text(`子组件状态: ${this.isActive ? '激活' : '未激活'}`)
  }
}

父组件 (Parent.ets)

typescript

@Entry
@Component
struct ParentComponent {
  // 父组件自己的状态变量
  @State isActive: boolean = false;

  build() {
    Column() {
      // 使用 $ 符将父组件的状态变量传递给子组件的 @Link 变量
      ChildComponent({ isActive: $isActive })

      Button('激活子组件')
        .onClick(() => {
          // 修改父组件的状态,子组件的 @Link 变量会自动同步,并触发其 aboutToUpdate
          this.isActive = true;
        })
    }
  }
}
3. Controller 代理模式

这种方式适合需要为复杂组件提供一个清晰、可复用的API的场景。

子组件 (Child.ets)

typescript

// 1. 定义子组件的控制器类
export class ChildController {
  public doSomething: () => void = () => {};
}

@Component
export struct ChildComponent {
  // 2. 接收父组件传入的控制器实例
  controller?: ChildController;

  private handleSomething() {
    console.log('子组件正在执行具体操作');
  }

  aboutToAppear() {
    // 3. 在组件即将出现时,将内部方法绑定到控制器上
    if (this.controller) {
      this.controller.doSomething = this.handleSomething.bind(this);
    }
  }

  build() {
    Text('我是子组件')
  }
}

父组件 (Parent.ets)

typescript

@Entry
@Component
struct ParentComponent {
  // 1. 实例化控制器
  private childController: ChildController = new ChildController();

  callChild() {
    // 2. 通过控制器调用子组件的方法
    this.childController.doSomething();
  }

  build() {
    Column() {
      // 3. 将控制器实例传给子组件
      ChildComponent({ controller: this.childController })
      Button('通过Controller调用子组件')
        .onClick(() => {
          this.callChild();
        })
    }
  }
}

总结

总的来说,@Ref 是解决“父组件如何直接调用子组件方法”这一问题的最常规选择。而在其他不同场景下,选择的原则如下:

  • 需要主动调用:首选 @Ref,简单且高效。

  • 需要同步状态:使用 @Link 或 @Prop + @Event,这是鸿蒙推荐的数据驱动方式。

  • 组件设计复杂,需要对外暴露API:考虑 Controller 代理模式,使代码更清晰。

  • 跨越多层或非父子组件:使用 emitter 事件总线。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐