文章目录前言Watch状态变化时搞点事实战搜索框防抖Watch 的常见坑Track精确到字段的追踪实战表单验证联动我的使用心得前言做搜索框的时候你是不是也这么干过用户每敲一个字就发一次请求一秒钟打了十几个接口调用这体验太糙了。鸿蒙的Watch装饰器就是为这种场景准备的——状态一变就触发回调你在回调里加个防抖就搞定了。再加上Track做精确字段追踪可以避免很多不必要的 UI 刷新。今天把这两个装饰器彻底讲清楚附带几个真实踩坑案例。Watch状态变化时搞点事Watch的核心用法很简单监听一个State变量值变了就执行回调函数。Componentstruct CounterPage{StateWatch(onCountChange)count:number0Statemessage:string还没开始onCountChange(){if(this.count10){this.message超过 10 了}elseif(this.count5){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最常见的用法了。Componentstruct SearchBar{StateWatch(onQueryChange)query:stringStateresults:string[][]StateisSearching:booleanfalseprivatedebounceTimer:number-1onQueryChange(){// 清掉上一次的定时器if(this.debounceTimer!-1){clearTimeout(this.debounceTimer)}// 空值直接清空结果if(this.query.trim()){this.results[]this.isSearchingfalsereturn}this.isSearchingtrue// 500ms 防抖this.debounceTimersetTimeout((){this.doSearch(this.query)},500)}asyncdoSearch(keyword:string){// 模拟网络请求awaitnewPromisevoid((resolve)setTimeout(resolve,300))// 模拟搜索结果this.results[${keyword}的结果 1,${keyword}的结果 2,${keyword}的结果 3,]this.isSearchingfalse}build(){Column({space:12}){Row(){TextInput({placeholder:搜索...,text:this.query}).onChange((value:string){this.queryvalue}).layoutWeight(1)if(this.isSearching){LoadingProgress().width(24).height(24).margin({left:8})}}.width(100%)if(this.results.length0){List(){ForEach(this.results,(item:string){ListItem(){Text(item).fontSize(15).padding(12)}})}}}.padding(16)}}这里有个细节防抖的 timer 我用的是普通变量不是State。因为 timer id 本身不需要驱动 UI 刷新。Watch 的常见坑坑一循环触发。回调里改了另一个被Watch监听的变量那边又触发回调改回来死循环了。// 反面教材StateWatch(onAChange)a:number0StateWatch(onBChange)b:number0onAChange(){this.bthis.a*2// 触发 onBChange}onBChange(){this.athis.b/2// 又触发 onAChange死循环}解决办法要么去掉其中一个Watch要么在回调里加条件判断值没变就不继续执行。坑二回调里干太多事。Watch的回调是同步执行的你在里面做一堆耗时操作会卡 UI。重活交给异步处理回调里只负责发起。坑三和生命周期搞混。Watch在aboutToAppear之前不会触发但在aboutToDisappear之后如果还有异步回调跑回来组件已经销毁了。保险起见在aboutToDisappear里清掉所有 timer 和未完成的请求。Track精确到字段的追踪Observed类的所有可观测属性变更都会触发 UI 刷新。但有时候你只关心其中一两个字段其他字段变了不需要刷新。Track就是干这个的——标记在Observed类的特定属性上只有被标记的属性变化才会触发使用到这个属性的组件刷新。来看个表单的例子ObservedclassFormData{Trackusername:stringTrackemail:stringTrackphone:stringTracklastModified:numberDate.now()// 这个变化不需要刷新 UIlogs:string[][]// 没加 Track 也不会触发刷新updateField(field:string,value:string){if(fieldusername)this.usernamevalueif(fieldemail)this.emailvalueif(fieldphone)this.phonevaluethis.lastModifiedDate.now()this.logs.push(Updated${field}at${this.lastModified})}}注意这里有个关键区别当Observed类的所有属性都加了Track那就只有被Track标记的属性变更才触发刷新。没加Track的属性比如上面的lastModified和logs变更了不会影响 UI。子组件用ObjectLink接收Componentstruct UsernameField{ObjectLinkform:FormDatabuild(){Row(){Text(用户名).width(80)TextInput({text:this.form.username}).onChange((value:string){this.form.updateField(username,value)})}}}Componentstruct EmailField{ObjectLinkform:FormDatabuild(){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结合起来做个带实时验证的注册表单ObservedclassRegisterForm{Trackusername:stringTrackemail:stringTrackusernameError:stringTrackemailError:string}Componentstruct RegisterPage{Stateform:RegisterFormnewRegisterForm()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.usernameErrorthis.form.emailErrorthis.form.username!this.form.email!?#3498db:#ccc).enabled(this.form.usernameErrorthis.form.emailErrorthis.form.username!this.form.email!)}.padding(20).width(100%)}}验证逻辑放在子组件里用Watch触发Componentstruct UsernameField{ObjectLinkform:RegisterForm// 监听 username 变化做验证Watch(validateUsername)_watchTrigger:number0aboutToAppear(){// 初始不需要触发验证}validateUsername(){constnamethis.form.usernameif(name.length0){this.form.usernameError}elseif(name.length3){this.form.usernameError用户名至少 3 个字符}elseif(name.length20){this.form.usernameError用户名不能超过 20 个字符}else{this.form.usernameError}}build(){Row(){Text(用户名).width(80)TextInput({text:this.form.username}).onChange((value:string){this.form.usernamevaluethis.validateUsername()}).layoutWeight(1)}}}这样改了用户名验证错误信息只影响用户名那一行邮箱区域完全不受影响。Track保证了字段级别的精确刷新。我的使用心得Watch和Track这两个装饰器解决的问题其实不一样Watch是数据变了要去做某件事——侧重副作用触发。防抖、验证、联动、日志都是它的活。Track是别刷不该刷的地方——侧重性能优化。数据模型字段多了以后不加Track的话改一个字段全组件树跟着抖体验很差。两者配合起来用效果最好。Track管该不该刷Watch管刷完之后做点什么。把这两个职责分清楚状态管理的代码就好写多了。