4、组件通信方式:父传子、子传父、兄弟通信、跨层级通信

组件化开发的核心,说白了就是让组件之间能「好好说话」。

我刚开始写小程序时,最头疼的就是数据怎么在组件之间流转。明明两个组件长得差不多,数据就是传不过去。后来踩了不少坑,才把这几条通信路子摸清楚。

今天咱们就把这四种通信方式掰开揉碎了讲。你想想看,组件就像一个个独立的小房间,数据就是房间里的人。怎么让人从一个房间走到另一个房间?嗯,这就是通信要解决的问题。

4.1 父传子:properties 传参

这是最基础、最常用的通信方式。父组件通过 properties 把数据传给子组件。

核心思路:父组件在子组件标签上写属性,子组件在 properties 里声明接收。

我个人习惯把 properties 叫做「组件的公开接口」。你想想看,一个组件对外暴露了哪些属性,别人就知道该怎么用它。

父组件代码示例:

<!-- 父组件页面 -->
<view>
  <my-child user-name="{{userInfo.name}}" age="{{userInfo.age}}"></my-child>
</view>

子组件接收:

// 子组件的 js
Component({
  properties: {
    userName: {
      type: String,
      value: '默认用户'
    },
    age: {
      type: Number,
      value: 0
    }
  }
})

小技巧:properties 支持的类型有 String、Number、Boolean、Object、Array。我建议你尽量把类型写明确,别偷懒用 null。为什么?因为类型检查能帮你提前发现很多低级错误。

我在项目中遇到过一个问题:父组件传了一个对象,子组件里修改了对象的某个属性,结果父组件的数据也跟着变了。这是因为对象是引用传递。嗯,这里要注意,如果你不希望子组件修改父组件的数据,可以在子组件里深拷贝一份再用。

4.2 子传父:triggerEvent 触发事件

子组件怎么跟父组件说话?靠 triggerEvent 触发自定义事件。

说白了,就是子组件喊一声「我这儿有事发生了」,父组件听到后自己去处理。

子组件触发事件:

// 子组件 js
Component({
  methods: {
    onTap() {
      // 触发自定义事件,把数据带出去
      this.triggerEvent('myevent', { id: 1, name: '张三' })
    }
  }
})

父组件监听事件:

<!-- 父组件页面 -->
<my-child bind:myevent="onChildEvent"></my-child>
// 父组件 js
Page({
  onChildEvent(e) {
    console.log('子组件传来的数据:', e.detail)
    // e.detail 就是 { id: 1, name: '张三' }
  }
})

避坑指南:我曾经犯过一个错误——在子组件里直接调用父组件的方法。后来发现这样耦合太严重,组件根本没法复用。正确的做法是用 triggerEvent 把事件抛出去,父组件想怎么处理就怎么处理。

triggerEvent 的第二个参数是 detail,也就是传给父组件的数据。第三个参数可以配置事件的一些行为,比如是否冒泡、是否穿越组件边界等。不过日常开发中,前两个参数基本够用了。

4.3 兄弟组件通信

兄弟组件之间没有直接的父子关系,怎么通信?

我常用的方式有三种:

  • 方式一:通过共同的父组件中转 —— 兄弟 A 把数据传给父组件,父组件再传给兄弟 B。这是最稳妥的方式。
  • 方式二:使用全局数据 —— 比如 app.globalData 或者 getApp() 共享数据。
  • 方式三:事件总线 —— 自己实现一个简单的发布订阅模式。

方式一示例:

<!-- 父组件 -->
<view>
  <brother-a bind:dataChange="onDataChange"></brother-a>
  <brother-b data="{{sharedData}}"></brother-b>
</view>
// 父组件 js
Page({
  data: {
    sharedData: null
  },
  onDataChange(e) {
    this.setData({
      sharedData: e.detail
    })
  }
})

我个人比较推荐方式一,因为数据流向清晰,出了问题好排查。方式二虽然简单,但全局变量容易污染,调试起来很头疼。

我的经验:如果兄弟组件之间通信频繁,可以考虑用 mobx 或者自己封装一个 store。但项目不大时,用父组件中转就足够了。别过度设计。

4.4 跨层级通信

跨层级通信,说白了就是爷孙组件、甚至更深的嵌套关系怎么传数据。

小程序提供了两种方式:

  • relations 关系 —— 在组件定义时声明父子关系,然后通过 getRelationNodes 获取关联的组件实例。
  • selectComponent 选择器 —— 通过 id 或 class 直接获取某个子组件的实例。

selectComponent 示例:

// 父组件 js
Page({
  onReady() {
    // 获取子组件实例
    const child = this.selectComponent('#myChild')
    // 直接调用子组件的方法
    child.someMethod()
  }
})

relations 方式:

// 父组件
Component({
  relations: {
    './child': {
      type: 'child',
      linked(target) {
        // 当子组件被链接时触发
        console.log('子组件链接成功', target)
      }
    }
  }
})

// 子组件
Component({
  relations: {
    './parent': {
      type: 'parent',
      linked(target) {
        console.log('父组件链接成功', target)
      }
    }
  }
})

注意:relations 方式需要组件之间明确知道对方的路径,而且必须在同一个页面内。我曾经在一个复杂表单里用 relations 做跨层级通信,结果因为组件嵌套太深,relations 的 linked 回调没触发。排查了半天才发现是路径写错了。

说实话,跨层级通信在小程序里用得不多。大部分场景用父传子 + 子传父就能解决。如果真的遇到跨层级需求,我建议优先考虑 selectComponent,简单直接。

4.5 总结对比

通信方式 适用场景 优点 缺点
父传子 (properties) 父组件向子组件传递数据 简单、类型安全 只能单向传递
子传父 (triggerEvent) 子组件通知父组件 解耦、灵活 事件名需要约定
兄弟通信 (父中转) 兄弟组件之间传递数据 数据流向清晰 需要父组件参与
跨层级通信 爷孙组件或深层嵌套 直接获取实例 耦合度高、易出错

嗯,这四种方式基本覆盖了小程序组件通信的所有场景。你想想看,掌握了这些,以后写组件就像搭积木一样,想怎么拼就怎么拼。

我个人建议:能用 properties 和 triggerEvent 解决的,就别用其他方式。保持简单,才是长久之道。