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 解决的,就别用其他方式。保持简单,才是长久之道。