harmonyOs 实用方法(一)父组件调用子组件方法
方案对比
| 方案 | 核心机制 | 数据流向 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|---|---|
| 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事件总线。
