HarmonyOS7 @Watch 和 @Track 怎么用才不乱刷?监听与精确刷新讲透
前言
做搜索框的时候,你是不是也这么干过:用户每敲一个字就发一次请求,一秒钟打了十几个接口调用?这体验太糙了。
鸿蒙的 @Watch 装饰器就是为这种场景准备的——状态一变就触发回调,你在回调里加个防抖就搞定了。再加上 @Track 做精确字段追踪,可以避免很多不必要的 UI 刷新。
今天把这两个装饰器彻底讲清楚,附带几个真实踩坑案例。
@Watch:状态变化时搞点事
@Watch 的核心用法很简单:监听一个 @State 变量,值变了就执行回调函数。
@Component
struct CounterPage {
@State @Watch('onCountChange') count: number = 0
@State message: string = '还没开始'
onCountChange() {
if (this.count > 10) {
this.message = '超过 10 了!'
} else if (this.count > 5) {
this.message = '过半了,继续加油'
} else {
this.message = `当前 ${this.count}`
}
}
build() {
Column({ space: 16 }) {
Text(this.message)
.fontSize(18)
Button(`+1 (${this.count})`)
.onClick(() => { this.count++ })
}
}
}

@Watch 的回调函数名写成字符串参数。当 count 变化时,onCountChange 自动执行。
注意:@Watch 在组件初始化时不会触发,只在后续值变更时才执行。这个设计很合理——初始化阶段你在 aboutToAppear 里该干嘛干嘛就好。
实战:搜索框防抖
实际开发中,搜索防抖大概是 @Watch 最常见的用法了。
@Component
struct SearchBar {
@State @Watch('onQueryChange') query: string = ''
@State results: string[] = []
@State isSearching: boolean = false
private debounceTimer: number = -1
onQueryChange() {
// 清掉上一次的定时器
if (this.debounceTimer !== -1) {
clearTimeout(this.debounceTimer)
}
// 空值直接清空结果
if (this.query.trim() === '') {
this.results = []
this.isSearching = false
return
}
this.isSearching = true
// 500ms 防抖
this.debounceTimer = setTimeout(() => {
this.doSearch(this.query)
}, 500)
}
async doSearch(keyword: string) {
// 模拟网络请求
await new Promise<void>((resolve) => setTimeout(resolve, 300))
// 模拟搜索结果
this.results = [
`${keyword} 的结果 1`,
`${keyword} 的结果 2`,
`${keyword} 的结果 3`,
]
this.isSearching = false
}
build() {
Column({ space: 12 }) {
Row() {
TextInput({ placeholder: '搜索...', text: this.query })
.onChange((value: string) => {
this.query = value
})
.layoutWeight(1)
if (this.isSearching) {
LoadingProgress()
.width(24)
.height(24)
.margin({ left: 8 })
}
}
.width('100%')
if (this.results.length > 0) {
List() {
ForEach(this.results, (item: string) => {
ListItem() {
Text(item)
.fontSize(15)
.padding(12)
}
})
}
}
}
.padding(16)
}
}


这里有个细节:防抖的 timer 我用的是普通变量,不是 @State。因为 timer id 本身不需要驱动 UI 刷新。
@Watch 的常见坑
坑一:循环触发。回调里改了另一个被 @Watch 监听的变量,那边又触发回调改回来,死循环了。
// 反面教材
@State @Watch('onAChange') a: number = 0
@State @Watch('onBChange') b: number = 0
onAChange() {
this.b = this.a * 2 // 触发 onBChange
}
onBChange() {
this.a = this.b / 2 // 又触发 onAChange,死循环
}
解决办法:要么去掉其中一个 @Watch,要么在回调里加条件判断,值没变就不继续执行。
坑二:回调里干太多事。@Watch 的回调是同步执行的,你在里面做一堆耗时操作会卡 UI。重活交给异步处理,回调里只负责"发起"。
坑三:和生命周期搞混。@Watch 在 aboutToAppear 之前不会触发,但在 aboutToDisappear 之后如果还有异步回调跑回来,组件已经销毁了。保险起见,在 aboutToDisappear 里清掉所有 timer 和未完成的请求。
@Track:精确到字段的追踪
@Observed 类的所有可观测属性变更都会触发 UI 刷新。但有时候你只关心其中一两个字段,其他字段变了不需要刷新。
@Track 就是干这个的——标记在 @Observed 类的特定属性上,只有被标记的属性变化才会触发使用到这个属性的组件刷新。
来看个表单的例子:
@Observed
class FormData {
@Track username: string = ''
@Track email: string = ''
@Track phone: string = ''
@Track lastModified: number = Date.now() // 这个变化不需要刷新 UI
logs: string[] = [] // 没加 @Track 也不会触发刷新
updateField(field: string, value: string) {
if (field === 'username') this.username = value
if (field === 'email') this.email = value
if (field === 'phone') this.phone = value
this.lastModified = Date.now()
this.logs.push(`Updated ${field} at ${this.lastModified}`)
}
}
注意这里有个关键区别:当 @Observed 类的所有属性都加了 @Track,那就只有被 @Track 标记的属性变更才触发刷新。没加 @Track 的属性(比如上面的 lastModified 和 logs),变更了不会影响 UI。
子组件用 @ObjectLink 接收:
@Component
struct UsernameField {
@ObjectLink form: FormData
build() {
Row() {
Text('用户名')
.width(80)
TextInput({ text: this.form.username })
.onChange((value: string) => {
this.form.updateField('username', value)
})
}
}
}
@Component
struct EmailField {
@ObjectLink form: FormData
build() {
Row() {
Text('邮箱')
.width(80)
TextInput({ text: this.form.email })
.onChange((value: string) => {
this.form.updateField('email', value)
})
}
}
}
改了 username,只有 UsernameField 会刷新。EmailField 纹丝不动——因为它只依赖 form.email,而 email 没变。
实战:表单验证联动
把 @Watch 和 @Track 结合起来,做个带实时验证的注册表单:
@Observed
class RegisterForm {
@Track username: string = ''
@Track email: string = ''
@Track usernameError: string = ''
@Track emailError: string = ''
}
@Component
struct RegisterPage {
@State form: RegisterForm = new RegisterForm()
build() {
Column({ space: 20 }) {
Text('注册')
.fontSize(22)
.fontWeight(FontWeight.Bold)
// 用户名
Column() {
UsernameField({ form: this.form })
if (this.form.usernameError !== '') {
Text(this.form.usernameError)
.fontSize(12)
.fontColor('#e74c3c')
}
}
// 邮箱
Column() {
EmailField({ form: this.form })
if (this.form.emailError !== '') {
Text(this.form.emailError)
.fontSize(12)
.fontColor('#e74c3c')
}
}
// 提交按钮
Button('注册')
.width('100%')
.backgroundColor(
this.form.usernameError === '' &&
this.form.emailError === '' &&
this.form.username !== '' &&
this.form.email !== ''
? '#3498db' : '#ccc'
)
.enabled(
this.form.usernameError === '' &&
this.form.emailError === '' &&
this.form.username !== '' &&
this.form.email !== ''
)
}
.padding(20)
.width('100%')
}
}
验证逻辑放在子组件里用 @Watch 触发:
@Component
struct UsernameField {
@ObjectLink form: RegisterForm
// 监听 username 变化,做验证
@Watch('validateUsername') _watchTrigger: number = 0
aboutToAppear() {
// 初始不需要触发验证
}
validateUsername() {
const name = this.form.username
if (name.length === 0) {
this.form.usernameError = ''
} else if (name.length < 3) {
this.form.usernameError = '用户名至少 3 个字符'
} else if (name.length > 20) {
this.form.usernameError = '用户名不能超过 20 个字符'
} else {
this.form.usernameError = ''
}
}
build() {
Row() {
Text('用户名')
.width(80)
TextInput({ text: this.form.username })
.onChange((value: string) => {
this.form.username = value
this.validateUsername()
})
.layoutWeight(1)
}
}
}
这样改了用户名,验证错误信息只影响用户名那一行,邮箱区域完全不受影响。@Track 保证了字段级别的精确刷新。
我的使用心得
@Watch 和 @Track 这两个装饰器,解决的问题其实不一样:
@Watch 是"数据变了要去做某件事"——侧重副作用触发。防抖、验证、联动、日志,都是它的活。
@Track 是"别刷不该刷的地方"——侧重性能优化。数据模型字段多了以后,不加 @Track 的话改一个字段全组件树跟着抖,体验很差。
两者配合起来用效果最好。@Track 管"该不该刷",@Watch 管"刷完之后做点什么"。把这两个职责分清楚,状态管理的代码就好写多了。
更多推荐

所有评论(0)