ArkTS 进阶之道(11):build() 重渲边界——状态变为啥只刷依赖组件
本文是「ArkTS 进阶之道」系列第 11 篇,开「ArkUI 渎染哲学」新阶段。上一阶段讲状态哲学(篇 56-59):数据流绑定(@State/@Prop/@Link/@Provide/@Consume)+ 状态变回调(@Watch)——都是状态绑追踪。本文换角度讲渲染:build() 重渲边界——根因在依赖追踪的局部重渲机制,@State � 变只刷依赖 UI 不刷全 build,不是全组件树重渲。能力系列篇 17 讲过条件渲染怎么用,本文讲为哈只刷依赖不刷全 build——根因在局部重渲边界。
一、开篇:build() 不是全重渲,是依赖追踪的局部重渲边界
你写 TypeScript/React 时,状态变重渲是「魔法」(React 谰 render 全函数重渲,要 useMemo/useCallback 手写 memo 避免全刷):
// React 全函数重渲 + 手写 memo 避免全刷 function Component() { const [count, setCount] = useState(0) const staticText = '我不依赖 count' // 不依赖 count 但全函数重渲时也重渲 return ( <View> <Text>{count}</Text> ← 依赖 count,count 变刷 <Text>{staticText}</Text> ← 不依赖 count,但 React 全函数重渲时也重渲(要 useMemo) </View> ) } setCount(1) // React 谰 render 全函数重渲,staticText 也重渲(要手写 memo 避免)你写鸿蒙 ArkTS 时,build()依赖追踪的局部重渲——不用手写 memo 避免全刷:
// ArkTS build() 依赖追踪局部重渲边界 @Entry @Component struct Index { @State count: number = 0 staticText: string = '我不依赖 count,count 变我不刷' // 不依赖 count,count �变不刷 build() { Column() { Text(`${this.count}`) ← 依赖 count,count �变刷(局部重渲) Text(this.staticText) ← 不依赖 count,count 变不刷(重渲边界) } } } // count � 变只刷依赖 count 的 Text,staticText 不刷(局部重渲边界,不用手写 memo)魔法 vs 边界的区别:React 把重渲当「魔法」(全函数重渲,你手写 memo 避免全刷),ArkTS 把 build() 当「依赖追踪的局部重渲边界」(装饰器编译期记依赖,状态变只刷依赖 UI 不刷全 build)。根因不是魔法是边界——@State 变触发的是局部重渲(只刷依赖 UI),不是全 build 重渲。
二、根因:build() 的依赖追踪局部重渲边界机制
鸿蒙 ArkUI 的 build() 是依赖追踪局部重渲边界——编译期记 UI 哪里依赖哪个 @State,@State 变只刷依赖 UI 不刷全 build,来自三重边界机制。
机制 1:编译期记 UI 依赖——build 里哪行 UI 用了哪个 @State
build() 编译期记 UI 依赖——扫描 build() 里 UI 哪行用了哪个 @State,记成依赖关系:
@Entry @Component struct Index { @State count: number = 0 // @State 荬饰器,编译期加追踪 staticText: string = '我不依赖 count' // 普通字段,不记依赖 build() { Column() { Text(`${this.count}`) ← 编译期记:Text1 依赖 count Text(this.staticText) ← 编译期记:Text2 不依赖 count(依赖 staticText) } } }编译期记依赖:build() 编译期扫描每行 UI——Text(${this.count})依赖 @Statecount记成count → Text1,Text(this.staticText)依赖普通字段staticText记成staticText → Text2。记依赖不用手写 memo,装饰器编译期扫描记。根因不是全重渲是编译期记的局部依赖关系。
机制 2:状态变只刷依赖 UI——@State 参触发局部重渲不刷全 build
@State 变只刷依赖 UI——赋值触发只重渲编译期记的依赖 UI,不刷全 build:
@Entry @Component struct Index { @State count: number = 0 staticText: string = '我不依赖 count,count 变我不刷' build() { Column() { Text(`${this.count}`) ← 依赖 count:count 变刷 Text1(局部重渲) Text(this.staticText) ← 不依赖 count:count 变不刷 Text2(重渲边界) Button('点我') .onClick(() => { this.count++ }) ← count 反 反只刷 Text1,Text2 不刷 } } }局部重渲边界:@Statecount赋值this.count++触发只重渲编译期记的依赖 UIText1(显示新 count),不刷不依赖 count 的Text2(staticText 不刷)。赋值只刷依赖 UI 是局部重渲边界——不是全 build 重渲,是编译期记的依赖关系触发局部刷。根因不是全刷是依赖追踪的局部重渲边界。
机制 3:边界证据——不依赖变 UI 不刷 / 普通字段不刷
build() 重渲边界证据——不依赖变 UI 不刷,普通字段不刷 UI:
@Entry @Component struct Index { @State count: number = 0 @State staticText: string = '我不依赖 count,count 变我不刷(重渲边界证据)' renderCount: number = 0 // 普通字段不刷 UI(重渲边界对比) build() { Column() { Text(`依赖 count = ${this.count}`) ← 依赖 count:count 反 刷 Text(this.staticText) ← 不依赖 count:count 反 反不刷(边界证据) Text(`普通字段 = ${this.renderCount}`) ← 普通字段:赋值不刷 UI(边界对比) Button('改 count') .onClick(() => { this.count++ }) ← count 反 反只刷依赖 count UI Button('改 staticText') .onClick(() => { this.staticText = '改了' }) ← staticText 反只刷 Text2 } } }边界证据:①改 count 只刷依赖 count 的 Text1,不依赖 count 的 Text2 不刷(局部重渲边界)②改 staticText 只刷依赖 staticText 的 Text2,依赖 count 的 Text1 不刷(边界反向证据)③普通字段 renderCount 赋值不刷 UI(不是 @State 没追踪不刷)。三个证据证明 build() 是依赖追踪的局部重渲边界,不是全 build 重渲。
三、真机配图:build() 重渲边界——状态变只刷依赖组件不刷全 build
初始态(count=0、staticText 显示「我不依赖 count」、@Watch 回调次数=0 域初始值):
点调两按钮后(count=1 刷依赖 UI、staticText 改了刷自己不刷依赖 count UI、@Watch 回调次数=1 普通字段不刷 UI 域重渲边界对比证据齐):
对比证据:点改 @State count 按钮后只刷依赖 count 的 UI(count=1 刷了),不依赖 count 的 staticText 没刷(仍显示原值)是重渲边界证据。点改 staticText 按钮后只刷 staticText 自己,依赖 count 的 UI 没刷(边界反向证据)。@Watch 回调次数=1 是普通字段赋值不刷 UI(边界对比)。build() 不是全重渲是依赖追踪的局部重渲边界——@State 反只刷依赖 UI 不刷全 build,根因在编译期记的依赖关系触发局部刷。
四、真解法:build() 重渲边界的三个场景
场景 1:状态变只刷依赖 UI(90% 场景,局部重渲边界首选)
@Entry @Component struct Index { @State count: number = 0 // @State 编译期记依赖 build() { Column() { Text(`count = ${this.count}`) ← 依赖 count:count 反刷 Text('我不依赖 count') ← 不依赖 count:count 反不刷(边界) Button('点我') .onClick(() => { this.count++ }) ← 只刷依赖 count 的 Text } } }为哈能跑:@Statecount编译期记依赖,赋值只刷依赖 UI 不刷全 build。首选这个,90% 的场景状态变只刷依赖 UI 用局部重渲边界就够。要写「状态变只刷相关 UI 不刷全 build」时用这个——不用手写 memo 避免全刷,装饰器编译期记依赖触发局部刷。
场景 2:多状态各刷各依赖 UI(每个 @State 只刷自己的依赖 UI)
@Entry @Component struct Index { @State count: number = 0 // count 只刷依赖 count UI @State name: string = '初始' // name 只刷依赖 name UI @State list: string[] = [] // list 只刷依赖 list UI build() { Column() { Text(`count = ${this.count}`) ← 依赖 count:count 反只刷这行 Text(`name = ${this.name}`) ← 依赖 name:name 反只刷这行 Text(`list = ${this.list.length}`) ← 依赖 list:list 反只刷这行 Button('改 count').onClick(() => { this.count++ }) // 只刷依赖 count UI Button('改 name').onClick(() => { this.name = '新名' }) // 只刷依赖 name UI Button('改 list').onClick(() => { this.list.push('新') }) // 只刷依赖 list UI } } }为哈能跑:每个 @State 只刷自己的依赖 UI——count 变只刷依赖 count 的 Text,name 变只刷依赖 name 的 Text,list 变只刷依赖 list 的 Text。要写「多状态各自只刷自己 UI 不互刷」时用这个——每个 @State 编译期记自己依赖,赋值只刷自己依赖 UI 不刷别的。
场景 3:@Watch 改普通字段不刷 UI(副作用边界 vs 数据流边界)
@Entry @Component struct Index { @State @Watch('onCountChange') count: number = 0 // @State + @Watch renderCount: number = 0 // 普通字段:@Watch 回调里改,不刷 UI onCountChange(): void { this.renderCount++ // @Watch 回调里改普通字段(不刷 UI,副作用边界) } build() { Column() { Text(`count = ${this.count}`) ← 依赖 count:count 反刷(数据流边界) Text(`renderCount = ${this.renderCount}`) ← 依赖普通字段:普通字段不刷 UI(边界对比) Button('点我') .onClick(() => { this.count++ }) // count 反刷依赖 UI + @Watch 改 renderCount 不刷 } } }为哈能跑:@Watch 回调里改普通字段renderCount不刷 UI——普通字段不是 @State 没追踪不刷(副作用边界)。要写「状态变要执行副作用但不刷 UI」时用这个——@Watch 回调改普通字段执行副作用(计数/日志),不触发 UI 重渲(副作用边界 vs 数据流边界分离)。
五、一句话哲学
build() 不是全重渲,是依赖追踪的局部重渲边界。ArkUI 的 build() 编译期记 UI 哪行依赖哪个 @State,@State 反只刷依赖 UI 不刷全 build。根因不是全重渲是局部重渲边界——build() 编译期记依赖(扫描记依赖关系)+ 状态变只刷依赖 UI(局部重渲不刷全 build)+ 边界证据(不依赖变 UI 不刷/普通字段不刷)。对比 React 全函数重渲要手写 memo 避免全刷,ArkUI 荬饰器编译期记依赖触发局部刷。
状态哲学→渲染哲学过渡:状态哲学(篇 56-59)讲数据流绑定 + 状态变回调——都是「状态绑追踪触发」。渲染哲学(篇 60-62)讲 build() 重渲边界——状态变触发的局部重渲是「渲染绑依赖」。@State 赋值触发的两个边界:①数据流边界(篇 56,赋值就刷 UI 依赖追踪)②渲染边界(篇 60,只刷依赖 UI 不刷全 build)——渲染边界是数据流边界的细化:不只刷 UI,是只刷依赖 UI。
下一篇:ArkTS 进阶之道(12)—— if/条件渲染边界:为啥 build 里不能写 if 语句(对应能力系列篇 17,讲根因)——续「ArkUI 渎染哲学」阶段。
能力系列回链
| 能力系列篇 | 本文进阶点 |
|---|---|
| 篇 17 条件渲染用法 | 下篇预告:if 条件渲染边界根因 |
| 篇 13 @State 基础用法 | 状态哲学:@State �赋值就刷 UI 依赖追踪 |
| 篇 16 @Watch 用法 | 状态哲学:@Watch �响应式回调钩子 |
真机 demo 完整代码
// 篇 60 demo:build() 重渲边界——状态变只刷依赖组件不刷全 build // 对比:@State � 反只刷依赖 UI vs 普通字段变不刷 UI @Entry @Component struct Index { // ✅ @State 依赖追踪:变只刷依赖 UI 不刷全 build @State @Watch('onCountChange') count: number = 0 @State staticText: string = '我不依赖 count,count 反我不刷(重渲边界证据)' @State log: string = '(未操作)' renderCount: number = 0 // 普通字段不刷 UI(重渲边界对比) onCountChange(): void { this.renderCount++ // @Watch 回调里改普通字段(不刷 UI) this.log = `count 反=${this.count},@Watch 回调调了 ${this.renderCount} 次(staticText 不刷是重渲边界证据)` } build() { Column({ space: 12 }) { Text('篇 60 配图:build() 重渲边界——状态变只刷依赖组件') .fontSize(18).fontWeight(FontWeight.Bold).margin({ top: 20, bottom: 8 }) Text('@State count 反只刷依赖 UI 不刷全 build(重渲边界证据)') .fontSize(12).fontColor('#888').margin({ bottom: 16 }) Column({ space: 6 }) { // 依赖 count 的 UI:count 反就刷 Text(`依赖 count = ${this.count}`).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#2563eb') // 不依赖 count 的 UI:count 反不刷(重渲边界证据) Text(this.staticText).fontSize(13).fontColor('#999') Text(`@Watch 回调次数 = ${this.renderCount}(普通字段不刷 UI)`).fontSize(13).fontColor('#999') Text(`日志:${this.log}`).fontSize(12).fontColor('#333').margin({ top: 4 }) } .width('92%').padding(12).backgroundColor('#f5f5f5').borderRadius(8) // 依赖 count 的按钮:count 反刷依赖 UI Button('改 @State count(只刷依赖 UI)') .width('92%').height(44).fontSize(14) .onClick(() => { this.count++ // ✅ count 反只刷依赖 UI,staticText 不刷(重渲边界) }) // 改不依赖 count 的 staticText:只刷 staticText 不刷依赖 count 的 UI Button('改 staticText(只刷 staticText 不刷依赖 count UI)') .width('92%').height(44).fontSize(14) .onClick(() => { this.staticText = `改了!count=${this.count}(我没刷,因 count 没反)` this.log = `改 staticText,只刷 staticText 不刷依赖 count UI(重渲边界)` }) } .width('100%').height('100%').alignItems(HorizontalAlign.Center) } }写鸿蒙 ArkUI 记住:build() 不是全重渲是依赖追踪的局部重渲边界——build() 编译期记 UI 哪行依赖哪个 @State,@State 反只刷依赖 UI 不刷全 build。根因不是全重渲是局部重渲边界——build() 编译期记依赖(扫描记依赖关系)+ 状态变只刷依赖 UI(局部重渲不刷全 build)+ 边界证据(不依赖变 UI 不刷/普通字段不刷)。状态变只刷依赖 UI 用局部重渲边界(首选),多状态各刷各依赖 UI 不互刷,@Watch 改普通字段执行副作用不刷 UI(副作用边界 vs 数据流边界分离)。只刷依赖 UI 不刷全 build是 ArkUI 渎染哲学核心!