小程序自定义组件的 properties、observers、behaviors 和生命周期如何理解?
简化版
自定义组件用 properties 接收外部数据,用 data 管内部状态,用 methods 定义方法,用 observers 监听属性或数据变化,用 lifetimes 和 pageLifetimes 管组件与页面生命周期,用 behaviors 复用组件逻辑。核心是分清“外部传入”和“内部状态”,避免直接修改 props 式数据。
详细版
组件基础结构:
Component({
properties: {
value: String
},
data: {
innerValue: ''
},
lifetimes: {
attached() {},
detached() {}
},
observers: {
value(v) {
this.setData({ innerValue: v })
}
},
methods: {
onTap() {
this.triggerEvent('change', { value: this.data.innerValue })
}
}
})
behaviors 类似 mixin,可复用属性、数据、方法和生命周期,但命名冲突和隐式来源要控制。组件对外通信常用 triggerEvent,不要让组件直接修改父页面数据。
完整版教学
一、自定义组件解决的是复用和边界
小程序页面复杂后,会出现大量重复 UI 和交互:商品卡片、弹窗、筛选栏、输入框、导航栏。自定义组件的作用是把结构、样式、逻辑封装在一起,通过属性和事件与外部交互。
父页面
-> properties 传入数据
-> 组件内部渲染和交互
-> triggerEvent 把结果通知父页面
这个边界和 Vue/React 组件思想类似:父级负责业务数据,组件负责局部表现和交互。面试回答时不要只背字段名,要讲“输入、内部状态、输出”三段。
如果组件直接操作父页面数据,短期省事,长期会让依赖关系变乱。复用组件最重要的是对外接口稳定。
二、properties 是外部输入,data 是内部状态
properties 用来接收父级传入的数据,data 用来维护组件自己的状态。例如一个弹窗组件,visible 可以来自外部,动画状态、临时输入值可以放内部。
Component({
properties: {
visible: Boolean,
title: String
},
data: {
loading: false
}
})
| 类型 | 来源 | 是否建议组件内部随意改 | 示例 |
|---|---|---|---|
properties | 父级传入 | 不建议 | visible、value |
data | 组件内部 | 可以通过 setData 改 | loading、activeIndex |
externalClasses | 外部样式注入 | 不属于数据 | 自定义样式类 |
如果组件需要实现类似受控输入,可以把外部 value 同步到内部 innerValue,用户输入后通过 triggerEvent 通知父级更新。这样数据流比较清楚。
三、observers 用来响应数据变化
observers 可以监听 properties 或 data 的变化,适合做派生状态同步、格式化、联动计算。它不是业务流程入口,也不适合放重逻辑。
Component({
properties: {
start: Number,
end: Number
},
observers: {
'start,end'(start, end) {
this.setData({ duration: end - start })
}
}
})
假设 start=10、end=25,observer 计算出 duration=15。这类派生字段比较适合 observer,因为它由输入数据决定。
但 observer 里如果继续频繁 setData 大对象,可能引发额外渲染成本。更糟的是 observer A 改字段 B,observer B 又改字段 A,就可能形成难排查的循环。
易错点:observer 是“响应变化”的工具,不是“替代所有业务逻辑”的工具。计算要轻,依赖要清楚。
四、lifetimes 和 pageLifetimes 分别管不同生命周期
组件自身生命周期用 lifetimes,常见有 created、attached、ready、detached。组件所在页面生命周期影响组件时,可以用 pageLifetimes,例如页面显示、隐藏、尺寸变化。
Component({
lifetimes: {
attached() {
this.init()
},
detached() {
this.dispose()
}
},
pageLifetimes: {
show() {},
hide() {},
resize(size) {}
}
})
如果组件里注册了定时器、事件监听、长连接或全局订阅,应在 detached 中释放。假设一个页面列表里创建 100 个组件,每个组件都注册一个全局监听却不清理,页面反复进入 10 次后可能积累 1000 个监听器。
生命周期回答要带资源管理意识:初始化在哪里做,什么时候能访问节点,什么时候清理副作用。
五、methods 和 triggerEvent 定义组件输出
组件内部方法写在 methods 中。组件要通知父级时,使用 triggerEvent 触发自定义事件,由父级在 WXML 中绑定。
<price-input value="{{price}}" bind:change="onPriceChange" />
// component
this.triggerEvent('change', { value: 1299 })
// page
Page({
onPriceChange(e) {
this.setData({ price: e.detail.value })
}
})
事件 detail 是组件输出的主要载体。这样父级可以决定是否接受变更、是否校验、是否请求接口。组件不需要知道父级业务细节。
这也是面试中“父子通信”的高级版:父传子靠 properties,子传父靠 triggerEvent,复杂跨层通信再考虑状态管理或事件总线,但不能一上来就乱用全局对象。
六、behaviors 是复用能力,也会带来隐式复杂度
behaviors 可以复用组件的 properties、data、methods、生命周期等能力。它适合抽取多个组件共同的逻辑,例如表单校验、主题能力、埋点能力。
const trackBehavior = Behavior({
methods: {
track(name) {
console.log('track', name)
}
}
})
Component({
behaviors: [trackBehavior],
methods: {
onTap() {
this.track('tap')
}
}
})
它的风险是来源不直观。一个方法到底来自组件自身还是 behavior,读代码时需要跳转。多个 behavior 出现同名字段时,还要理解合并和覆盖规则。
| 复用方式 | 优点 | 风险 |
|---|---|---|
| 组件组合 | 边界清晰 | 层级可能增加 |
| behavior | 复用逻辑方便 | 隐式来源、命名冲突 |
| 工具函数 | 简单直接 | 无生命周期能力 |
工程上不要把 behavior 做成“万能混入”。可复用但要小而清晰,并保持命名规范。
七、常见误区与追问
- 误区:properties 和 data 没区别。 properties 是外部输入,data 是内部状态,数据流边界不同。
- 误区:组件可以直接改父页面数据。 组件应通过
triggerEvent通知父级,由父级决定如何更新。 - 误区:observers 适合写复杂业务流程。 observers 适合轻量派生和同步,重逻辑会增加循环和性能风险。
- 追问:attached 和 ready 有什么区别?
attached表示组件进入节点树,ready更适合需要布局完成后的操作。 - 追问:behaviors 和工具函数怎么选? 需要属性、生命周期、方法混入时用 behavior;纯计算逻辑优先工具函数。
- 追问:组件资源在哪里清理? 定时器、订阅、全局监听等应在
detached中释放。
八、加强记忆
自定义组件按“输入、状态、输出、复用、清理”记:properties 是输入,data 是状态,triggerEvent 是输出,behaviors 是复用,detached 管清理。再用 observers 做轻量派生,用生命周期安排初始化和销毁,组件题就能答得像工程经验而不是字段背诵。