ArkTS 进阶之道(14):@Builder 组件复用边界——为啥 build 不能当函数调

0 阅读1分钟

ArkTS 进阶之道(14):@Builder 组件复用边界——为啥 build 不能当函数调

本文是「ArkTS 进阶之道」系列第 14 篇,开「ArkUI 组件设计」阶段(系列收官)。上一阶段讲渲染哲学(篇 60-62):渲染树绑定(build/if/ForEach �绑渲染树节点/分支/循环项)。本文讲组件复用:@Builder 贬饰器复用 UI 片段——根因在绑渲染树节点不是返值函数,@Builder 贬饰器绑渲染树节点复用 UI,build 不能当函数调返 UI。能力系列篇 19 讲过 @Builder 怎么用,本文讲为哈 @Builder 复用合法 build 当函数调报错——根因在渲染树节点绑定不是返值函数。

一、开篇:@Builder 不是返值函数,是绑渲染树节点的复用贬饰器

你写 TypeScript/React 时,UI 片段复用是「魔法」(React 要定义子组件/函数组件返 JSX):

// React 函数组件返 JSX 复用 UI 片段
function MyTextBlock({ label, value }: { label: string; value: number }) {
  return (
    <View>
      <Text>{label}</Text>
      <Text>{value}</Text>
    </View>
  )    ← �返 JSX 值,是返值函数
}
function Component() {
  return (
    <View>
      <MyTextBlock label="复用1" value={1} />    ← 调函数组件返 JSX 嵌
      <MyTextBlock label="复用2" value={2} />
    </View>
  )
}
// React 用函数组件返 JSX 复用,是返值函数魔法

你写鸿蒙 ArkTS 时,@Builder 绑渲染树节点复用——不用返值函数:

// ArkTS @Builder 绑渲染树节点复用 UI 片段
@Entry
@Component
struct Index {
  @Builder
  MyTextBlock(label: string, value: number) {    ← @Builder 贬饰器,绑渲染树节点
    Column() {
      Text(label)
      Text(`${value}`)
    }
  }
  build() {
    Column() {
      this.MyTextBlock('复用1', 1)    ← 调 @Builder 绑渲染树节点1(不返值)
      this.MyTextBlock('复用2', 2)    ← 调 @Builder 绑渲染树节点2(不返值)
    }
  }
}
// @Builder 绑渲染树节点复用 UI 片段,不是返值函数(不返 JSX)

魔法 vs 贬饰器的区别:React 把 UI 复用当「魔法」(函数组件返 JSX 值),ArkTS 把 @Builder 当「绑渲染树节点贬饰器」(不返值,绑渲染树节点复用 UI)。根因不是魔法是渲染树节点绑定——@Builder 调用不返 UI 值,是绑渲染树节点的复用贬饰器。

二、根因:@Builder 的渲染树节点绑定复用机制

鸿蒙 ArkUI 的 @Builder 是渲染树节点绑定复用——编译期把 @Builder 方法绑成渲染树节点,调用点嵌节点复用 UI,不是返值函数,来自三重绑定机制。

机制 1:@Builder 编译期绑渲染树节点——不是返值函数

@Builder 贬饰器编译期绑渲染树节点——把 @Builder 方法编成渲染树的可复用节点,不是返 UI 值的函数:

@Entry
@Component
struct Index {
  @Builder
  MyTextBlock(label: string, value: number) {    ← @Builder 贬饰器编译期绑渲染树节点
    Column() {
      Text(label)
      Text(`${value}`)
    }
  }
  build() {
    Column() {
      this.MyTextBlock('复用1', 1)    ← 调用点嵌渲染树节点1(绑节点,不返值)
      this.MyTextBlock('复用2', 2)    ← 调用点嵌渲染树节点2(绑节点,不返值)
    }
  }
}
// 编译期:@Builder MyTextBlock 绑成渲染树可复用节点模板
// 调用点:this.MyTextBlock(...) 嵌渲染树节点实例(不返 UI 值)

编译期绑节点:@Builder MyTextBlock 贬饰器编译期把方法绑成渲染树的可复用节点模板——调用 this.MyTextBlock('复用1', 1) 在调用点嵌渲染树节点实例1(参数 label='复用1'/value=1),调用 this.MyTextBlock('复用2', 2) 嵌节点实例2。绑节点不返 UI 值,不是返值函数。根因不是返值是渲染树节点绑定复用。

机制 2:调用点嵌节点复用 UI——多个调用点绑多个节点实例

@Builder 贬饰器调用点嵌节点复用 UI——多个调用点各嵌渲染树节点实例,复用同一 @Builder 模板:

@Entry
@Component
struct Index {
  @State count: number = 0
  @Builder
  MyTextBlock(label: string, value: number) {    ← @Builder 模板(复用源)
    Column() {
      Text(label)
      Text(`值 = ${value}`)
    }
  }
  build() {
    Column() {
      this.MyTextBlock('复用1', this.count)        ← 调用点1 嵌节点实例1(label='复用1'/value=count)
      this.MyTextBlock('复用2', this.count * 10)   ← 调用点2 嵌节点实例2(label='复用2'/value=count*10)
      Button('改 count')
        .onClick(() => { this.count++ })          ← count 参两个节点实例都刷(绑节点追踪)
    }
  }
}
// 调用点1/调用点2 各嵌节点实例,复用同一 @Builder MyTextBlock 模板
// count 参两个节点实例都刷(绑渲染树节点追踪依赖)

调用点嵌节点复用:@Builder MyTextBlock 调用点1嵌节点实例1(label='复用1'/value=count),调用点2嵌节点实例2(label='复用2'/value=count*10)——两个实例复用同一 @Builder 模板,参数不同显不同 UI。count 参两个节点实例都刷(绑渲染树节点追踪依赖,篇 60 重渲边界机制)。根因不是返值拼接是调用点嵌节点复用。

机制 3:build 当函数调报错边界——返值表达式跟渲染树绑定冲突

build 里 @Builder 不能当函数调返值——@Builder 绑渲染树节点不返 UI 值,当函数调返值表达式跟渲染树绑定冲突报错:

@Entry
@Component
struct Index {
  @Builder
  MyTextBlock(label: string, value: number) {
    Column() { Text(label); Text(`${value}`) }
  }
  build() {
    Column() {
      // ❌ @Builder 不能当函数调返值(返值表达式跟渲染树绑定冲突)
      // const ui1 = this.MyTextBlock('复用1', 1)    ← 当函数调返值报错
      // return this.MyTextBlock('复用1', 1) + this.MyTextBlock('复用2', 2)    ← 返值拼接报错
      
      // ✅ @Builder 调用点嵌节点(不返值,绑渲染树节点)
      this.MyTextBlock('复用1', 1)    ← 调用点嵌节点,合法(不返值)
    }
  }
}
// @Builder 当函数调返值报错根因:@Builder 绑渲染树节点不返 UI 值
// 返值表达式(const ui = / return / + 拼接)跟渲染树绑定冲突报错

build 当函数调报错边界:@Builder 当函数调返值(const ui = / return / + 拼接)报错根因是 @Builder 绑渲染树节点不返 UI 值——返值表达式要求返 UI 值拼接,跟渲染树节点绑定冲突报错。@Builder 调用点嵌节点(不返值)合法,根因不是语法限制是渲染树节点绑定 vs 返值函数的边界冲突。

三、真机配图:@Builder 组件复用边界——绑渲染树节点复用 UI 片段

初始态(@Builder 复用1 显 count=0、复用2 显 count*10=0、分割线渲染均组件复用边界初始值):

@Builder 组件复用边界初始态

点调按钮后(@Builder 复用1/复用2 坟刷了 count=1/10、分割线渲染均组件复用边界对比证据齐):

@Builder 组件复用边界调用后态

对比证据:点改 count 按钮后 @Builder 复用1 显 count=1、复用2 显 count*10=10(两个调用点嵌的节点实例都刷了,绑渲染树节点追踪依赖)。@Builder 不是返值函数是绑渲染树节点的复用贬饰器——@Builder 贬饰器编译期绑渲染树节点模板,调用点嵌节点实例复用 UI 不返值。build 当函数调返值报错根因是 @Builder 绑渲染树节点不返 UI 值,返值表达式跟渲染树绑定冲突。

四、真解法:@Builder 组件复用的三个场景

场景 1:@Builder 带参数复用 UI 片段(90% 场景首选,多调用点复用)

@Entry
@Component
struct Index {
  @State count: number = 0
  @Builder
  MyTextBlock(label: string, value: number) {    ← @Builder 带参数复用模板
    Column() {
      Text(label)
      Text(`值 = ${value}`)
    }
  }
  build() {
    Column() {
      this.MyTextBlock('复用1', this.count)        ← 调用点1 嵌节点实例1
      this.MyTextBlock('复用2', this.count * 10)   ← 调用点2 嵌节点实例2
      Button('改 count')
        .onClick(() => { this.count++ })          ← count 参两个节点实例都刷
    }
  }
}

为哈能跑:@Builder 带参数复用 UI 片段——多个调用点嵌节点实例,参数不同显不同 UI。首选这个,90% 的场景 UI 片段复用用 @Builder 带参数就够。要写「同一 UI 片段多调用点复用显不同参数」时用这个——不用子组件/函数组件,@Builder 贬饰器绑节点模板调用点嵌实例。

场景 2:@Builder 无参数复用(分割线/装饰 UI 片段复用)

@Entry
@Component
struct Index {
  @Builder
  MyDivider() {                                  ← @Builder 无参数复用模板
    Divider().color('#eee').strokeWidth(1).margin(4)
  }
  build() {
    Column() {
      Text('区块1')
      this.MyDivider()                            ← 调用点1 嵌分割线节点1
      Text('区块2')
      this.MyDivider()                            ← 调用点2 嵌分割线节点2
      Text('区块3')
    }
  }
}

为哈能跑:@Builder 无参数复用——分割线/装饰 UI 片段多调用点嵌节点复用。要写「装饰 UI 片段(分割线/间距/装饰块)多调用点复用」时用这个——无参数 @Builder 绑节点模板,调用点嵌实例复用装饰 UI 不重复写。

场景 3:@Builder + 条件渲染嵌套(复用 UI 片段里嵌条件分支)

@Entry
@Component
struct Index {
  @State show: boolean = true
  @State count: number = 0
  @Builder
  MyStatusBlock(label: string, value: number) {
    Column() {
      Text(label)
      if (value > 0) {                            ← @Builder 里嵌 if 条件渲染
        Text(`正数:${value}`)
      } else {
        Text('非正')
      }
    }
  }
  build() {
    Column() {
      this.MyStatusBlock('状态1', this.count)     ← 调用点1 嵌节点实例1(含条件分支)
      this.MyStatusBlock('状态2', this.count - 5) ← 调用点2 嵌节点实例2(含条件分支)
      Button('改 count')
        .onClick(() => { this.count++ })
    }
  }
}

为哈能跑:@Builder + 条件渲染嵌套——@Builder 里嵌 if 条件渲染分支,复用 UI 片段含条件分支。要写「复用 UI 片段里有条件分支」时用这个——@Builder 绑节点模板嵌 if 贬饰器绑分支,调用点嵌实例含条件分支按参数选分支渲染。

五、一句话哲学

@Builder 不是返值函数,是绑渲染树节点的复用贬饰器。 ArkUI 的 @Builder 贬饰器编译期绑渲染树节点模板,调用点嵌节点实例复用 UI 不返值。根因不是返值函数是渲染树节点绑定——@Builder 编译期绑节点(渲染树可复用节点模板)+ 调用点嵌节点实例(多个调用点复用同一模板)+ build 当函数调报错边界(@Builder 绑节点不返 UI 值,返值表达式跟渲染树绑定冲突)。对比 React 函数组件返 JSX 值,ArkTS @Builder 绑渲染树节点不返值。

渲染哲学→组件设计过渡:渲染哲学(篇 60-62)讲渲染树绑定(build/if/ForEach 绑节点/分支/循环项)——都是「渲染树结构绑定」。组件设计(篇 63)讲 @Builder 绑渲染树节点复用——是「渲染树节点复用绑定」。@Builder 补上渲染哲学没讲的复用:不只绑渲染树结构,还能绑可复用节点模板调用点嵌实例。从「结构绑定」到「节点复用」扩展,根因都是编译期绑渲染树。

系列收官预告:本文是 ArkTS 进阶之道系列最后开的新阶段——「ArkUI 组件设计」阶段。下篇(篇 64,如续)讲 @BuilderParam/@Styles/@Extend 复用边界,或系列就此打住。五阶段哲学体系:类型哲学(50-52)→ 作用域哲学(53-55)→ 状态哲学(56-59)→ 渲染哲学(60-62)→ 组件设计(63+)讲清 ArkTS/ArkUI 进阶哲学。

能力系列回链

能力系列篇本文进阶点
篇 19 @Builder 用法@Builder 组件复用边界根因(绑渲染树节点)
篇 18 循环渲染用法上一篇:ForEach 循环渲染边界根因
篇 13 @State 基础用法状态哲学:@State 赋值就刷 UI 依赖追踪

真机 demo 完整代码

// 篇 63 demo:@Builder 组件复用 vs build 当函数调报错对比
// 对比:@Builder 贬饰器复用合法 vs 普通方法返 UI 报错

@Entry
@Component
struct Index {
  @State count: number = 0
  @State log: string = '(未操作)'

  // ✅ @Builder 贬饰器:复用 UI 片段,绑渲染树节点
  @Builder
  MyTextBlock(label: string, value: number) {
    Column({ space: 4 }) {
      Text(label).fontSize(12).fontColor('#888')
      Text(`值 = ${value}`).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#2563eb')
    }
    .alignItems(HorizontalAlign.Center)
  }

  // ✅ @Builder 带参数复用:多个调用点绑多个渲染树节点
  @Builder
  MyDivider() {
    Divider().color('#eee').strokeWidth(1).margin(4)
  }

  build() {
    Column({ space: 12 }) {
      Text('篇 63 配图:@Builder 组件复用边界')
        .fontSize(18).fontWeight(FontWeight.Bold).margin({ top: 20, bottom: 8 })
      Text('@Builder 复用 UI 片段 vs build 不能当函数调(对比证据)')
        .fontSize(12).fontColor('#888').margin({ bottom: 16 })

      Column({ space: 6 }) {
        Text(`count = ${this.count}`).fontSize(15).fontWeight(FontWeight.Bold)
        Text(`日志:${this.log}`).fontSize(12).fontColor('#333').margin({ top: 4 })
      }
      .width('92%').padding(12).backgroundColor('#f5f5f5').borderRadius(8)

      // ✅ @Builder 调用:复用 UI 片段,绑渲染树节点
      Text('@Builder 复用调用:').fontSize(13).fontColor('#2563eb')
      this.MyTextBlock('复用1', this.count)           // ✅ @Builder 调用,绑渲染树节点1
      this.MyDivider()                                // ✅ @Builder 分割线复用
      this.MyTextBlock('复用2', this.count * 10)      // ✅ @Builder 再调用,绑渲染树节点2

      // ❌ build 不能当普通函数调返 UI(对比证据,注释掉避编译炸)
      // this.MyTextBlock('复用1', this.count) + this.MyTextBlock('复用2', this.count * 10)
      // 普通方法返 UI 报错:build 是渲染树不是返值表达式

      Button('改 count(@Builder 复用点都刷)')
        .width('92%').height(44).fontSize(14)
        .onClick(() => {
          this.count++                // ✅ count 变 @Builder 复用点都刷(绑渲染树节点追踪)
          this.log = `count=${this.count}(@Builder 复用点都刷,绑渲染树节点追踪)`
        })
    }
    .width('100%').height('100%').alignItems(HorizontalAlign.Center)
  }
}

写鸿蒙 ArkUI 记住:@Builder 不是返值函数是绑渲染树节点的复用贬饰器——@Builder 贬饰器编译期绑渲染树节点模板,调用点嵌节点实例复用 UI 不返值。根因不是返值函数是渲染树节点绑定——@Builder 编译期绑节点(渲染树可复用节点模板)+ 调用点嵌节点实例(多个调用点复用同一模板)+ build 当函数调报错边界(@Builder 绑节点不返 UI 值,返值表达式跟渲染树绑定冲突)。@Builder 带参数复用 UI 片段用多调用点嵌实例显不同参数(首选),@Builder 无参数复用用装饰 UI 片段(分割线/间距),@Builder + 条件渲染嵌套用复用 UI 片段含条件分支。绑渲染树节点复用不返值是 ArkUI 组件设计哲学核心!