ArkTS 进阶之道(11):build() 重渲边界——状态变为啥只刷依赖组件

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 避免全刷):

typescript 复制代码
// 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 避免全刷:

typescript 复制代码
// 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,记成依赖关系:

typescript 复制代码
@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}) 依赖 @State count 记成 count → Text1Text(this.staticText) 依赖普通字段 staticText 记成 staticText → Text2。记依赖不用手写 memo,装饰器编译期扫描记。根因不是全重渲是编译期记的局部依赖关系。

机制 2:状态变只刷依赖 UI------@State 参触发局部重渲不刷全 build

@State 变只刷依赖 UI------赋值触发只重渲编译期记的依赖 UI,不刷全 build:

typescript 复制代码
@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 不刷
    }
  }
}

局部重渲边界 :@State count �赋值 this.count++ 触发只重渲编译期记的依赖 UI Text1(显示新 count),不刷不依赖 count 的 Text2(staticText 不刷)。赋值只刷依赖 UI 是局部重渲边界------不是全 build 重渲,是编译期记的依赖关系触发局部刷。根因不是全刷是依赖追踪的局部重渲边界。

机制 3:边界证据------不依赖变 UI 不刷 / 普通字段不刷

build() 重渲边界证据------不依赖变 UI 不刷,普通字段不刷 UI:

typescript 复制代码
@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% 场景,局部重渲边界首选)

typescript 复制代码
@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
    }
  }
}

为哈能跑 :@State count 编译期记依赖,赋值只刷依赖 UI 不刷全 build。首选这个,90% 的场景状态变只刷依赖 UI 用局部重渲边界就够。要写「状态变只刷相关 UI 不刷全 build」时用这个------不用手写 memo 避免全刷,装饰器编译期记依赖触发局部刷。

场景 2:多状态各刷各依赖 UI(每个 @State 只刷自己的依赖 UI)

typescript 复制代码
@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 数据流边界)

typescript 复制代码
@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 完整代码

typescript 复制代码
// 篇 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 渲染哲学核心!

相关推荐
xiaobaoyu1 小时前
mac+idea快捷键
后端
站大爷IP2 小时前
异常捕获别乱写!一个裸的 except: 让我调试了通宵,这3种写法差距太大了
后端
用户4279254051712 小时前
weibo-cli 实战:用命令行搭建微博自动化运营 Pipeline
后端
Oneslide2 小时前
kibana APM监控面板指标解析-I-JVM相关指标
后端
我不是AI2 小时前
Codex 装好了却用不了?API Key 与 KKFlow 配置精简教程
后端
用户208046804562 小时前
Flask 请求与响应新手实战指南
后端
程序员cxuan3 小时前
A 社官方:我们删掉了 80% 的 skills
人工智能·后端·程序员
苍何3 小时前
AI 短剧出海,门槛已经低到离谱了
后端
程序员黑豆3 小时前
鸿蒙应用开发:@Link 装饰器实现父子组件双向同步
前端·后端·harmonyos