小程序组件之间如何通信?
简化版
小程序组件通信常见方式有:父组件通过 properties 给子组件传值,子组件通过 triggerEvent 向父组件派发事件,父组件也可以通过 selectComponent 获取子组件实例调用方法。跨层级或全局场景可以使用状态管理、事件总线或全局数据,但要控制复杂度。
详细版
父传子一般使用 properties:
Component({
properties: {
title: String
}
})
子组件向父组件传递信息时,使用 this.triggerEvent('change', data),父组件在 WXML 中绑定事件。
如果父组件需要主动调用子组件方法,可以给子组件设置 id,然后通过 this.selectComponent('#child') 获取实例。但这种方式耦合更强,应谨慎使用。
对于兄弟组件或跨多层组件通信,可以把状态提升到共同父组件,也可以使用全局 store。小项目中简单事件就够,大项目要避免事件链太长导致维护困难。
完整版教学
一、父组件向子组件传值
小程序组件的外部输入通过 properties 定义。父组件使用类似属性绑定的方式传入数据,子组件内部通过 this.data.xxx 使用。
优点是关系清晰,数据来源明确。适合展示型组件,例如按钮、卡片、弹窗、列表项。
需要注意的是,子组件不应该随意修改父组件传入的属性。如果需要修改,应通过事件通知父组件,由父组件更新数据后再传回来。
二、子组件向父组件通信
子组件没有直接修改父组件状态的权限,它应该通过事件表达“发生了什么”。
this.triggerEvent('select', {
id: item.id
})
父组件监听:
<custom-item bind:select="handleSelect" />
这种方式符合单向数据流:父组件传数据,子组件发事件。数据变化最终仍由父组件决定。
三、父组件调用子组件方法
selectComponent 可以拿到子组件实例:
const dialog = this.selectComponent('#dialog')
dialog.open()
它适合命令式场景,比如打开弹窗、重置表单、播放动画。但如果大量业务状态都靠调用子组件方法同步,组件之间会变得强耦合。
判断标准很简单:如果它是 UI 行为,可以考虑实例方法;如果它是业务数据变化,优先用数据和事件。
四、面试追问与工程落地
面试官常问:“兄弟组件之间怎么通信?”
首选方案是状态提升。把共享状态放到共同父组件,父组件分别传给两个子组件,一个子组件通过事件通知父组件,父组件再更新另一个子组件。
如果层级很深,可以使用全局状态管理。但要避免为了一个小状态引入过重方案。小程序项目尤其要注意页面栈和组件生命周期,事件监听要在销毁时清理,否则可能出现重复触发或内存泄漏。
五、属性、事件与更新边界
properties 会成为组件数据的一部分,父组件更新绑定值后,框架把新值传给子组件。子组件即使通过 setData 临时改了同名字段,也没有修改父组件中的源数据,父级下一次下发还可能覆盖它;因此可编辑值通常复制为内部状态,再通过事件请求父级确认。
| 通信方向 | 推荐机制 | 适合内容 | 主要代价 |
|---|---|---|---|
| 父 → 子 | properties | 展示数据、配置 | 父级更新会触发下发与渲染 |
| 子 → 父 | triggerEvent | 用户意图、变化结果 | 事件名和 detail 要形成契约 |
| 父 → 子命令 | selectComponent | 打开、聚焦、重置等 UI 动作 | 父级依赖子组件实现 |
| 多组件共享 | 状态提升或 store | 跨层业务状态 | 引入统一状态生命周期 |
triggerEvent(name, detail, options) 的第三个参数可控制冒泡、跨组件边界等行为,默认不应假设自定义事件一定像 DOM 事件那样冒泡。事件载荷只传必要字段,例如只传 { id: 42, checked: true },不要把整个列表作为 detail 反复复制。
六、跨层通信与耦合控制
假设页面有 4 层组件,最底层修改筛选条件。如果逐层转发,需要 4 份属性/事件契约;共享状态很多时可以引入 store,但只有一个弹窗开关时,状态提升或实例方法更清楚。选型依据是状态由谁拥有、多少消费者需要、生命周期是否一致,而不是层级一深就必然上全局状态。
单向数据流:父状态 → properties → 子组件
↓ 用户操作
triggerEvent
↓
父更新状态
复杂父子组件还可用 relations 声明组件关系,组件可以在关联建立和变化时获取对方实例;它适合树、表单项等结构性关系,不适合替代业务状态管理。事件总线若确实使用,必须在组件或页面卸载时解除监听,并规定事件来源与载荷,否则 3 个页面各注册一次后,同一事件可能被重复处理 3 次。
记忆钩子:数据用“父下发、子上报”,命令才考虑实例调用;共享状态的归属越模糊,组件耦合越高。
七、常见误区与追问
- 误区:子组件修改
properties就能同步修改父组件状态。 子组件只改了自身数据副本,父级源状态不会因此变化,仍应通过事件通知父级更新。 - 误区:所有自定义事件都会自动冒泡到任意祖先组件。 是否冒泡、是否跨组件边界由事件选项与组件边界决定,不能套用 DOM 的默认印象。
- 误区:
selectComponent比属性和事件更高效,所以可以统一使用。 它暴露子组件实例并形成命令式耦合,只适合明确的 UI 动作。 - 追问:兄弟组件通信的首选方案是什么? 把共享状态提升到共同父组件,由一个子组件上报、父组件更新后再下发给另一个子组件。
- 追问:属性变化时子组件如何执行派生逻辑? 可使用数据监听器
observers,并避免在监听中无条件写回同一字段造成重复更新。 - 追问:什么时候使用全局 store? 多个页面或远距离组件长期共享业务状态、且需要统一变更入口时才值得引入。
- 追问:事件总线为什么容易泄漏? 订阅者随页面反复创建,若卸载时不解绑,旧回调仍被引用并在后续事件中重复执行。
八、加强记忆
组件通信按“所有权”来记:父组件拥有数据就用 properties 下发,子组件只通过 triggerEvent 报告意图;打开弹窗、重置表单等 UI 命令才用 selectComponent。跨层状态先尝试提升,消费者多且生命周期明确时再使用 store;relations 管结构关系,事件总线则必须配套解绑和事件契约。