3、组件通信(上):父传子(properties)、子传父(triggerEvent)、兄弟组件通信
组件通信,说白了就是组件之间怎么「说话」。
我刚开始写小程序时,觉得组件化就是拆文件。后来发现,拆开容易,让它们配合起来才是真功夫。今天咱们先聊三种最基础的通信方式:父传子、子传父、兄弟组件通信。
3.1 父传子:properties
这是最直观的一种通信。父组件把数据往下扔,子组件伸手接住就行。
核心思路:父组件在标签上写属性,子组件用 properties 声明接收。
举个例子。我有一个商品卡片组件,父页面要把商品信息传给它。
子组件定义(product-card/index):
// product-card.js
Component({
properties: {
// 简单写法
title: String,
price: Number,
// 完整写法(我推荐这种,可读性更好)
product: {
type: Object,
value: {},
observer: function(newVal, oldVal) {
console.log('商品数据变了', newVal)
}
}
}
})
<!-- product-card.wxml -->
<view class="card">
<text>{{title}}</text>
<text>¥{{price}}</text>
<image src="{{product.image}}"></image>
</view>
父组件使用:
<!-- 父页面.wxml -->
<product-card
title="蓝牙耳机"
price="199"
product="{{productData}}"
/>
我的习惯:能用对象传就别拆成单个属性。比如 product 对象一次传完,比 title、price、image 一个个传清爽多了。我在项目中遇到过传了十几个属性的情况,改起来想哭。
3.2 子传父:triggerEvent
子组件想跟父组件说话,怎么办?不能直接改父组件的数据,那样就乱套了。
小程序的做法是:子组件「喊一声」,父组件「听着」。这个喊的过程就叫 triggerEvent。
子组件触发事件:
// product-card.js
Component({
methods: {
onAddToCart() {
// 第一个参数:事件名
// 第二个参数:要传递的数据
// 第三个参数:配置(一般用默认)
this.triggerEvent('addcart', {
id: this.properties.product.id,
name: this.properties.title
})
}
}
})
<!-- product-card.wxml -->
<button bindtap="onAddToCart">加入购物车</button>
父组件监听事件:
<!-- 父页面.wxml -->
<product-card
title="蓝牙耳机"
bind:addcart="onAddCartHandler"
/>
// 父页面.js
Page({
onAddCartHandler(event) {
const { id, name } = event.detail
console.log('收到子组件消息:', id, name)
// 这里可以更新购物车数据
}
})
我曾经踩过的坑:事件名不要用驼峰,比如 addCart。小程序里事件名统一用小写,否则 bind:addCart 可能监听不到。改成 bind:addcart 就稳了。
3.3 兄弟组件通信
兄弟组件之间怎么通信?说白了,它们没有直接关系,不能直接传。
常用的方案有三种:
| 方案 | 适用场景 | 复杂度 |
|---|---|---|
| 通过父组件中转 | 兄弟数量少,关系简单 | 低 |
| 全局事件总线 | 兄弟多,层级深 | 中 |
| 全局数据(app.globalData) | 跨页面、跨组件共享 | 中 |
方案一:父组件中转(我最常用的)
兄弟 A 触发事件给父组件,父组件再通过 properties 传给兄弟 B。
<!-- 父组件.wxml -->
<!-- 兄弟A:搜索框 -->
<search-box bind:search="onSearch" />
<!-- 兄弟B:商品列表 -->
<product-list keyword="{{keyword}}" />
// 父组件.js
Component({
data: {
keyword: ''
},
methods: {
onSearch(event) {
// 兄弟A说:我搜了"耳机"
// 父组件更新数据,兄弟B自动收到
this.setData({
keyword: event.detail.keyword
})
}
}
})
方案二:全局事件总线
小程序没有内置的事件总线,但我们可以用 app 对象或者自己写一个。
// app.js
App({
globalData: {
eventBus: {
listeners: {},
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = []
}
this.listeners[event].push(callback)
},
emit(event, data) {
this.listeners[event]?.forEach(cb => cb(data))
},
off(event) {
delete this.listeners[event]
}
}
}
})
// 兄弟A组件.js
const app = getApp()
Component({
methods: {
sendMessage() {
app.globalData.eventBus.emit('updateList', { id: 1 })
}
}
})
// 兄弟B组件.js
const app = getApp()
Component({
lifetimes: {
attached() {
app.globalData.eventBus.on('updateList', (data) => {
console.log('收到兄弟消息:', data)
this.setData({ list: data })
})
},
detached() {
// 别忘了清理,防止内存泄漏
app.globalData.eventBus.off('updateList')
}
}
})
我的建议:兄弟通信优先用父组件中转。虽然代码多写几行,但数据流向清晰,调试起来不头疼。全局事件总线用多了,项目会变成「事件地狱」,你想想看,到处都在 emit 和 on,出了问题都不知道谁发的。
3.4 避坑指南
- properties 是单向的:子组件不要直接修改 properties 的值。虽然小程序不报错,但会警告。真想改,自己 data 里存一份副本。
- triggerEvent 的冒泡:默认不冒泡。如果你想让事件穿透组件层,第三个参数传
{ bubbles: true }。 - 兄弟通信记得清理:用全局事件总线时,组件销毁了一定要 off 掉监听。我见过一个项目,页面切走了还在收消息,数据乱得没法看。
嗯,这三种通信方式搞明白了,大部分组件协作场景都能应付。下一节咱们聊更高级的通信方式,比如 selectComponent 和 relations。到时候你会发现,组件通信其实可以更灵活。