ArkTS 进阶之道(10):@Watch 状态监听——从回调时机理解响应式边界

ArkTS 进阶之道(10):@Watch 状态监听——从回调时机理解响应式边界

本文是「ArkTS 进阶之道」系列第 10 篇,「ArkUI 状态哲学」阶段收尾。前三篇讲状态绑定机制:@State 单组件内部(篇 56)→ @Prop/@Link 父子传值(篇 57)→ @Provide/@Consume 跨层传值(篇 58)——都是数据流绑定。本文讲状态变化的监听回调:@Watch 荬饰器——根因在响应式回调时机机制,@Watch 编译期绑状态变的回调钩子,状态变自动调不用手调。能力系列篇 16 讲过 @Watch 怎么用,本文讲为哈状态变自动调——根因在响应式回调时机。

一、开篇:@Watch 不是手调方法,是状态变的响应式回调钩子

你写 TypeScript/React 时,状态变监听是「魔法」(React useEffect 手写依赖数组监听状态变):

// React useEffect 手写依赖监听 const [count, setCount] = useState(0) useEffect(() => { console.log(`count 变为 ${count}`) ← 手写 useEffect + 依赖数组 [count] }, [count]) setCount(1) // count 变触发 useEffect 回调(手写依赖监听)

你写鸿蒙 ArkTS 时,@Watch 荬饰器响应式回调钩子——不用手写 useEffect 依赖数组:

// ArkTS @Watch 响应式回调钩子 @Entry @Component struct Index { @State @Watch('onCountChange') count: number = 0 // @Watch 绑状态变回调 onCountChange(): void { console.log(`count 变为 ${this.count}`) // 状态变自动调(不用手调) } build() { Button('点我') .onClick(() => { this.count++ }) // count 变触发 @Watch 自动调 onCountChange } }

魔法 vs 钩子的区别:React 把状态监听当「魔法」(你手写 useEffect + 依赖数组),ArkTS 把 @Watch 当「响应式回调钩子」(荬饰器编译期绑状态变回调,状态变自动调)。根因不是魔法是钩子——@Watch 编译期给 @State 绑回调方法名,状态变触发自动调绑的方法,不用手调。

二、根因:@Watch 的响应式回调时机机制

鸿蒙 ArkUI 的 @Watch 是响应式回调钩子——编译期给 @State 绑回调方法名,状态变触发自动调绑的方法,来自三重回调时机机制。

机制 1:@Watch 编译期绑回调方法名——状态变触发自动调

@Watch 荬饰器编译期绑回调方法名——给 @State 绑一个回调方法名,状态变触发自动调这个方法:

@Entry @Component struct Index { @State @Watch('onCountChange') count: number = 0 // @Watch 绑 'onCountChange' 方法名 onCountChange(): void { console.log(`count 变触发自动调:${this.count}`) // 回调方法,状态变自动调 } build() { Button('点我') .onClick(() => { this.count++ }) // count 变触发自动调 onCountChange } }

编译期绑方法名:@Watch('onCountChange')荬饰器编译期给 @Statecount绑回调方法名'onCountChange'——赋值this.count++触发自动调this.onCountChange()方法。绑方法名不用手写 useEffect 依赖数组,荬饰器编译期绑。根因不是手调是编译期绑的响应式钩子。

机制 2:状态变触发自动调——赋值就调不用手调

@Watch 荬饰器状态变触发自动调——赋值操作拦截成「赋值 + 自动调绑的回调方法」:

@Entry @Component struct Index { @State @Watch('onCountChange') count: number = 0 @State @Watch('onNameChange') name: string = '初始名' // 多个 @Watch 各绑各 onCountChange(): void { console.log(`count 变自动调:${this.count}`) } onNameChange(): void { console.log(`name 变自动调:${this.name}`) } build() { Button('改 count') .onClick(() => { this.count++ }) // count 变触发自动调 onCountChange Button('改 name') .onClick(() => { this.name = '新名' }) // name 变触发自动调 onNameChange } }

赋值拦截自动调:@Watchcount赋值this.count++被荬饰器拦截成「赋值 + 自动调 onCountChange」,@Watchname赋值this.name = '新名'被拦截成「赋值 + 自动调 onNameChange」。赋值就调不用手调,荬饰器绑的响应式钩子触发自动调。对比普通方法要手调(this.manualMethod()),@Watch 荬饰器赋值就自动调。

机制 3:回调时机是状态变后——响应式边界在赋值触发

@Watch 荬饰器回调时机是状态变后——赋值操作完成后触发回调,响应式边界在赋值触发:

@Entry @Component struct Index { @State @Watch('onCountChange') count: number = 0 onCountChange(): void { // 回调时机:count 赋值完成后调(状态变后) console.log(`回调调时 count 已变 = ${this.count}`) // this.count 已是新值 } build() { Button('点我') .onClick(() => { this.count++ // ① 赋值 count // ② 赋值完成后自动调 onCountChange(this.count 已是新值) // ③ 回调里 this.count 读到的是新值(状态变后) }) } }

回调时机状态变后:@Watch 回调时机是赋值完成后调——this.count++赋值完成后自动调onCountChange,回调里this.count读到的是新值(状态变后)。响应式边界在赋值触发——赋值操作完成 → 触发 @Watch 回调 → 回调里读到新值。根因不是同步监听是赋值完成后的响应式回调时机。

三、真机配图:@Watch 状态变自动调 vs 普通方法手动调对比证据

初始态(@Watch count=0、@Watch name=初始名、普通方法手动调次数=0 均初始值):

点调三按钮后(@Watch 自动调 onCountChange/onNameChange 日志显示、普通方法手动调次数=1 对比证据齐):

对比证据:点 @Watch 监 count++ 按钮后自动调 onCountChange(日志显示「@Watch 自动调:count 变为 1」不用手调),点 @Watch 监 name 改按钮后自动调 onNameChange(日志显示「@Watch 自动调:name 变为 名字1」不用手调),点普通方法按钮后手动调 manualMethod(次数=1 要手调)。@Watch 状态变自动调不是手调是响应式回调钩子——编译期绑方法名 + 赋值拦截自动调 + 回调时机状态变后三重机制。

四、真解法:@Watch 用法的三个场景

场景 1:状态变自动调回调(90% 场景首选,替代手调)

@Entry @Component struct Index { @State @Watch('onCountChange') count: number = 0 // @Watch 绑状态变回调 onCountChange(): void { console.log(`count 变为 ${this.count}`) // 状态变自动调(不用手调) } build() { Button('点我') .onClick(() => { this.count++ }) // count 变触发自动调 onCountChange } }

为哈能跑:@Watch 编译期绑回调方法名,状态变触发自动调绑的方法。首选这个,90% 的场景状态变要回调用 @Watch 自动调就够。要写「状态变要执行副作用(日志/缓存/通知)」时用这个——不用手调,赋值就自动调绑的回调方法。

场景 2:多状态各绑各回调(每个 @State 绑自己的 @Watch)

@Entry @Component struct Index { @State @Watch('onCountChange') count: number = 0 // count 绑 onCountChange @State @Watch('onNameChange') name: string = '初始名' // name 绑 onNameChange @State @Watch('onListChange') list: string[] = [] // list 绑 onListChange onCountChange(): void { console.log(`count 变:${this.count}`) } onNameChange(): void { console.log(`name 变:${this.name}`) } onListChange(): void { console.log(`list 变:${this.list.length}`) } build() { Column() { Button('改 count').onClick(() => { this.count++ }) // 触发 onCountChange Button('改 name').onClick(() => { this.name = '新名' }) // 触发 onNameChange Button('改 list').onClick(() => { this.list.push('新') }) // 触发 onListChange } } }

为哈能跑:每个 @State 绑自己的 @Watch 回调方法名——count 变触发 onCountChange,name 变触发 onNameChange,list 变触发 onListChange。要写「多状态各自有副作用」时用这个——每个 @Watch 各绑各,状态变各触发各的回调,不用手调判断哪个状态变。

场景 3:@Watch vs 普通方法选型(自动调 vs 手调边界)

@Entry @Component struct Index { @State @Watch('onCountChange') count: number = 0 // @Watch 自动调(状态变触发) @State log: string = '' onCountChange(): void { this.log = `@Watch 自动调:count=${this.count}` // 响应式副作用(自动调) } manualMethod(): void { this.log = `普通方法手动调` // 命令式副作用(要手调) } build() { Column() { Button('改 count(@Watch 自动调)') .onClick(() => { this.count++ }) // @Watch 自动调 onCountChange Button('手调普通方法') .onClick(() => { this.manualMethod() }) // 普通方法要手调 } } }

为哈能跑:@Watch vs 普通方法选型按副作用触发方式——@Watch 响应式自动调(状态变触发,适合「状态变的副作用」),普通方法命令式手调(要手调,适合「按钮点击的一次性动作」)。要写「状态变的副作用(日志/缓存/通知)」时用 @Watch 自动调,要写「按钮点击的一次性动作」时用普通方法手调——按副作用触发方式选型,响应式自动调 vs 命令式手调边界。

五、一句话哲学

@Watch 不是手调方法,是状态变的响应式回调钩子。ArkUI 的 @Watch 荬饰器编译期给 @State 绑回调方法名,状态变触发自动调绑的方法不用手调。根因不是手调是响应式回调钩子——@Watch 编译期绑方法名(回调方法名绑定)+ 状态变触发自动调(赋值拦截自动调)+ 回调时机状态变后(响应式边界在赋值触发)。对比普通方法要手调,@Watch 荬饰器赋值就自动调是响应式回调钩子的直接证据。

状态哲学阶段收官串讲:@State 单组件内部(篇 56,赋值就刷 UI 依赖追踪)→ @Prop/@Link 父子传值(篇 57,单向 vs 双向数据流绑定)→ @Provide/@Consume 跨层传值(篇 58,键值注册跨层绑定)→ @Watch 状态监听(篇 59,响应式回调钩子)——四篇讲清 ArkUI 状态哲学:数据流绑定(@State/@Prop/@Link/@Provide/@Consume)+ 状态变回调(@Watch),根因都是编译期绑追踪/回调。

响应式边界总结:@State �赋值就刷 UI(数据流绑追踪范围)+ @Watch 赋值就调回调(回调钩子绑方法名)——赋值触发的两个响应式边界:①数据流边界(UI 重渲)②回调边界(副作用执行)。@Watch 补上 @State 没管的副作用边界——赋值不只刷 UI 还调 @Watch 回调执行副作用,完整响应式链路:赋值 → 刷 UI(@State)+ 调回调(@Watch)。

下一阶段预告:ArkUI 渲染哲学(篇 60-62)——从状态哲学(数据流+回调)到渲染哲学(build 重渲/条件渲染/循环渲染),换角度讲 ArkUI 渲染机制。

能力系列回链

能力系列篇本文进阶点
篇 16 @Watch 用法响应式回调时机根因
篇 15 @Provide/@Consume 用法上一篇:键值注册跨层绑定
篇 17 条件渲染用法下阶段预告:渲染哲学

真机 demo 完整代码

// 篇 59 demo:@Watch 状态监听回调时机 vs 普通方法调用对比 // 对比:@Watch 荬饰器状态变自动调 vs 普通方法手动调 @Entry @Component struct Index { // ✅ @Watch 荬饰器:监听 @State 变化自动调回调 @State @Watch('onCountChange') count: number = 0 @State @Watch('onNameChange') name: string = '初始名' @State log: string = '(未操作)' @State methodCallCount: number = 0 // 普通方法手动调次数(对比) // ✅ @Watch 回调:状态变自动调(不用手调) onCountChange(): void { this.log = `@Watch 自动调:count 变为 ${this.count}(不用手调)` } onNameChange(): void { this.log = `@Watch 自动调:name 变为 ${this.name}(不用手调)` } // ❌ 普通方法:手动调(对比证据) manualMethod(): void { this.methodCallCount++ this.log = `普通方法手动调:第 ${this.methodCallCount} 次(要手调)` } build() { Column({ space: 12 }) { Text('篇 59 配图:@Watch 状态监听回调时机') .fontSize(18).fontWeight(FontWeight.Bold).margin({ top: 20, bottom: 8 }) Text('@Watch 状态变自动调 vs 普通方法手动调(对比证据)') .fontSize(12).fontColor('#888').margin({ bottom: 16 }) Column({ space: 6 }) { Text(`@State @Watch count = ${this.count}`).fontSize(15).fontWeight(FontWeight.Bold) Text(`@State @Watch name = ${this.name}`).fontSize(15).fontWeight(FontWeight.Bold) Text(`普通方法手动调次数 = ${this.methodCallCount}`).fontSize(14).fontColor('#999') Text(`日志:${this.log}`).fontSize(12).fontColor('#333').margin({ top: 4 }) } .width('92%').padding(12).backgroundColor('#f5f5f5').borderRadius(8) Button('@Watch 监 count++(自动调 onCountChange)') .width('92%').height(44).fontSize(14) .onClick(() => { this.count++ // ✅ @Watch 自动调 onCountChange(不用手调) }) Button('@Watch 监 name 改(自动调 onNameChange)') .width('92%').height(44).fontSize(14) .onClick(() => { this.name = `名字${this.count}` // ✅ @Watch 自动调 onNameChange(不用手调) }) Button('普通方法手动调 manualMethod(要手调)') .width('92%').height(44).fontSize(14) .onClick(() => { this.manualMethod() // ❌ 普通方法要手调(对比证据) }) } .width('100%').height('100%').alignItems(HorizontalAlign.Center) } }

写鸿蒙 ArkUI 记住:@Watch 不是手调方法是状态变的响应式回调钩子——@Watch 荬饰器编译期给 @State 绑回调方法名,状态变触发自动调绑的方法不用手调。根因不是手调是响应式回调钩子——@Watch 编译期绑方法名(回调方法名绑定)+ 状态变触发自动调(赋值拦截自动调)+ 回调时机状态变后(响应式边界在赋值触发)。状态变要副作用(日志/缓存/通知)用 @Watch 自动调(首选),多状态各绑各 @Watch 各触发各回调,按钮一次性动作用普通方法手调。赋值不只刷 UI 还调 @Watch 回调执行副作用是 ArkUI 状态哲学核心收尾!