在 ArkUI 声明式开发中,UI 复用有三个选择:@Component@Builder@LocalBuilder。大部分人都知道 @Builder 轻量,但一碰到嵌套刷新失效this 指向错乱就束手无策。

本文从真实的翻车场景出发,配合完整可运行的代码,把这三个装饰器的设计原理、刷新机制、this 绑定一次性讲透。

一、公共数据类型定义

// model/UserInfo.ets
export interface UserInfo {
  name: string
  age: number
}

二、@Builder 的本质:它就是函数

@Builder 只是一个函数,调用它跟调用普通函数一模一样。

// 普通函数
function sayHello(name: string) {
  console.log('Hello ' + name)
}

// @Builder 函数
@Builder
function buildHello(name: string) {
  Text('Hello ' + name)
}

// 调用方式完全相同
sayHello('Tom')      // 普通函数调用
buildHello('Tom')    // @Builder 函数调用

唯一的区别是:@Builder 函数返回的是 UI 描述,会在编译期内联展开,不生成组件实例。

  • 编译期内联展开,不生成 VNode 实例
  • 不加入组件树,没有独立实例
  • 没有生命周期和状态管理
  • 调用它就是一次普通函数调用

核心认知@Builder 是函数,不是组件。

三、@Builder 的刷新机制

3.1 翻车现场 vs 正确姿势

// pages/InnerBuilderPage.ets
import { UserInfo } from '../model/UserInfo'

@Entry
@Component
struct InnerBuilderPage {
  @State user: UserInfo = { name: 'Tom', age: 18 }

  // ❌ 按值传递:不刷新
  @Builder
  showUser(params: UserInfo) {
    Text(`不刷新:${params.name} - ${params.age}`)
  }

  // ✅ 直接访问 @State 变量:刷新
  @Builder
  showUserDirect() {
    Text(`刷新:${this.user.name} - ${this.user.age}`)
  }

  build() {
    Column({ space: 15 }) {
      this.showUser(this.user)       // ❌ 点击按钮不刷新
      this.showUserDirect()          // ✅ 点击按钮刷新

      Button('年龄+1').onClick(() => {
        this.user.age++
      })
    }
    .justifyContent(FlexAlign.Center)
    .height('100%')
    .width('100%')
  }
}

关键前提showUserDirect() 能刷新的前提是 this.user 必须是 状态变量@State@Prop@Link@Observed 等可观测对象)。如果是普通成员变量,改变不会触发 UI 刷新。

效果演示

builder翻车现场vs正确姿势.gif

3.2 五种方式完整对比

// pages/InnerBuilderPage.ets
import { UserInfo } from '../model/UserInfo'

@Entry
@Component
struct InnerBuilderPage {
  @State count: number = 0
  @State user: UserInfo = { name: 'Tom', age: 18 }

  // ❌ 方式一:基础类型 → 不刷新
  @Builder
  buildLabel(label: string) {
    Text(`基础类型:${label}`).fontSize(16).fontColor('#999')
  }

  // ❌ 方式二:多参数 → 不刷新
  @Builder
  buildByMulti(name: string, age: number) {
    Text(`多参数:${name}${age}`).fontSize(16).fontColor('#999')
  }

  // ❌ 方式三:直接传对象变量 → 不刷新
  @Builder
  buildByObject(params: UserInfo) {
    Text(`直接传对象:${params.name}${params.age}`).fontSize(16).fontColor('#999')
  }

  // ✅ 方式四:对象字面量 → 可刷新
  @Builder
  buildByLiteral(params: UserInfo) {
    Text(`对象字面量:${params.name}${params.age}`).fontSize(16).fontColor('#34C85E')
  }

  // ✅ 方式五:无参数,直接访问 @State → 可刷新
  @Builder
  buildDirect() {
    Text(`无参数直接访问:${this.user.name}${this.user.age}`).fontSize(16).fontColor('#007DFF')
  }

  build() {
    Column({ space: 12 }) {
      Text('五种方式刷新对比').fontSize(20).fontWeight(FontWeight.Bold)

      this.buildLabel(`计数:${this.count}`)
      this.buildByMulti(this.user.name, this.user.age)
      this.buildByObject(this.user)
      this.buildByLiteral({ name: this.user.name, age: this.user.age })
      this.buildDirect()

      Row({ space: 10 }) {
        Button('count++').onClick(() => this.count++)
        Button('年龄+1').onClick(() => this.user.age++)
        Button('重置').onClick(() => {
          this.count = 0
          this.user = { name: 'Tom', age: 18 }
        })
      }

      Text('点击"年龄+1":后两行刷新,前三行不刷新').fontSize(14).fontColor('#999')
    }
    .padding(20)
    .width('100%')
  }
}

效果演示

builder五种传递方式对比.gif

3.3 刷新规则总结

方式 示例 是否刷新
基础类型传参 this.buildLabel(\计数:${this.count}`)`
多参数传值 this.buildByMulti(this.user.name, this.user.age)
直接传对象变量 this.buildByObject(this.user)
对象字面量 this.buildByLiteral({ name: this.user.name, age: this.user.age })
无参数,直接访问状态变量 this.buildDirect()(内部读 this.user

铁律:想让 @Builder 响应刷新,要么无参数直接访问状态变量,要么对象字面量传参。两者都能建立响应式依赖。

四、嵌套 Builder 的"雪崩效应"

// pages/NestedBuilderPage.ets
import { UserInfo } from '../model/UserInfo'

@Entry
@Component
struct NestedBuilderPage {
  @State user: UserInfo = { name: '张三', age: 25 }

  @Builder
  parentBuilder(params: UserInfo) {
    Column({ space: 10 }) {
      Text(`父Builder:${params.name}${params.age}`)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)

      // ✅ 对象字面量 → 子Builder刷新
      this.childBuilderOne({ name: params.name, age: params.age })

      // ✅ 父参数透传 → 子Builder刷新(父重建时传入新对象)
      this.childBuilderTwo(params)

      // ❌ 直接传递 @State 变量 → 子Builder不刷新(按值传递)
      this.childBuilderThree(this.user)

      // ✅ 子Builder内部直接访问 @State → 子Builder刷新
      this.childBuilderFour()
    }
    .padding(10)
    .backgroundColor('#f5f5f5')
    .borderRadius(8)
  }

  // ✅ 对象字面量传参 → 刷新
  @Builder
  childBuilderOne(params: UserInfo) {
    Text(`子(字面量):${params.name}${params.age}`).fontSize(16).fontColor('#007aff')
  }

  // ✅ 父参数透传 → 刷新
  @Builder
  childBuilderTwo(params: UserInfo) {
    Text(`子(透传):${params.name}${params.age}`).fontSize(16).fontColor('#007aff')
  }

  // ❌ 直接传递 @State 变量 → 不刷新
  @Builder
  childBuilderThree(params: UserInfo) {
    Text(`子(直接传递@State):${params.name}${params.age}`).fontSize(16).fontColor('#ff6b35')
  }

  // ✅ 无参数,内部直接访问 @State → 刷新
  @Builder
  childBuilderFour() {
    Text(`子(无参直接访问@State):${this.user.name}${this.user.age}`).fontSize(16).fontColor('#34C85E')
  }

  build() {
    Column({ space: 15 }) {
      Text('嵌套 Builder 刷新对比').fontSize(20).fontWeight(FontWeight.Bold)
      this.parentBuilder({ name: this.user.name, age: this.user.age })
      Button('年龄+1').onClick(() => this.user.age++)
      Text('点击按钮:前两个和第四个刷新,第三个不刷新').fontSize(14).fontColor('#999')
    }
    .padding(20)
    .width('100%')
  }
}

效果演示

builder嵌套刷新对比.gif

五、this 指向迷局

5.1 问题重现

@Builder 作为插槽传给子组件时:


@Component
struct Child {
  label: string = 'Child'
  @BuilderParam builder?: () => void

  build() {
    if (this.builder){
      this.builder?.()  // 在子组件中调用
    }
  }
}

@Entry
@Component
struct Parent {
  label: string = 'Parent'

  @Builder
  showLabel() {
    Text(this.label).fontSize(20)  // this 指向谁?
  }

  build() {
    Column(){
      Child({ builder: this.showLabel })
    }.width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

运行结果:显示 Child,不是 Parent

效果演示

builder_this指向问题重现.png
原因showLabel 在子组件中被调用,this 指向调用者——子组件。

5.2 两个关键知识点

this.normalBuilder vs this.normalBuilder()

// this.normalBuilder —— 函数引用(传的是函数本身,不执行)
@BuilderParam builder = this.normalBuilder

// this.normalBuilder() —— 函数调用(立即执行)
build() {
  this.normalBuilder()  // 立即执行,返回 UI
}

② 箭头函数没有自己的 this

箭头函数的 this 从定义时的外层作用域继承。

// 箭头函数在父组件 build() 中定义
arrowBuilder: () => { this.normalBuilder() }
// 箭头函数的 this 继承自父组件 → this 指向 Parent

5.3 三种修正方案完整对比

// pages/ThisBindingCompare.ets
@Component
struct Child {
  label: string = 'Child'

  @BuilderParam directBuilder?: () => void
  @BuilderParam arrowBuilder?: () => void
  @BuilderParam localBuilder?: () => void

  build() {
    Column({ space: 12 }) {
      Text('【子组件内执行结果】').fontSize(16).fontWeight(FontWeight.Bold)

      if (this.directBuilder) {
        Row({ space: 6 }) {
          Text('① 直接传 @Builder:').fontSize(14).fontColor('#999')
          this.directBuilder()
        }
      }

      if (this.arrowBuilder) {
        Row({ space: 6 }) {
          Text('② 箭头函数包裹:').fontSize(14).fontColor('#999')
          this.arrowBuilder()
        }
      }

      if (this.localBuilder) {
        Row({ space: 6 }) {
          Text('③ @LocalBuilder:').fontSize(14).fontColor('#999')
          this.localBuilder()
        }
      }
    }
    .padding(20)
    .backgroundColor('#f5f5f5')
    .borderRadius(12)
    .alignItems(HorizontalAlign.Start)
  }
}

@Entry
@Component
struct ThisBindingCompare {
  label: string = 'Parent'
  @State refreshKey: number = 0

  @Builder
  normalBuilder() {
    Text(`${this.label}`).fontSize(20).fontColor('#ff6b35')
  }

  @LocalBuilder
  localBuilder() {
    Text(`${this.label}`).fontSize(20).fontColor('#007DFF')
  }

  build() {
    Column({ space: 16 }) {
      Text('this 指向三种方式对比')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)

      Text(`父组件刷新次数:${this.refreshKey}`).fontSize(14).fontColor('#333')

      Child({
        // 方式①:直接传递函数引用
        // 子组件调用 this.directBuilder() 时,this 指向 Child
        directBuilder: this.normalBuilder,

        // 方式②:箭头函数包裹
        // 箭头函数继承父组件 this → 执行时 this 指向 Parent
        arrowBuilder: () => { this.normalBuilder() },

        // 方式③:@LocalBuilder
        // 编译期锁定 this 为 Parent
        localBuilder: this.localBuilder
      })

      Divider().margin(10)

      Button(`刷新父组件`)
        .onClick(() => {
          this.refreshKey++
        })
        .width('80%')
    }
    .padding(20)
    .width('100%')
    .alignItems(HorizontalAlign.Center)
  }
}

效果演示

builder三种传递方式this对比.png

5.4 对比结果

方式 写法 this 指向 函数引用
① 直接传 directBuilder: this.normalBuilder Child 稳定
② 箭头函数 arrowBuilder: () => { this.normalBuilder() } Parent 每次新函数
③ @LocalBuilder localBuilder: this.localBuilder Parent 稳定

5.5 为什么箭头函数能指向 Parent?

箭头函数没有自己的 this,它的 this 从定义时的外层作用域继承。

// 箭头函数在父组件的 build() 方法中定义
arrowBuilder: () => { this.normalBuilder() }
//                        ↑
// 箭头函数的 this 继承自父组件
// 所以 this.normalBuilder() 执行时 this 指向 Parent ✅

5.6 选型建议

方式 this 指向 引用稳定性 推荐度
直接传 @Builder ❌ 错乱 稳定 ❌ 不推荐
箭头函数包裹 ✅ 正确 ❌ 每次新函数 ⚠️ 可用
@LocalBuilder ✅ 正确 ✅ 稳定 推荐

六、@BuilderParam 插槽实战

// components/CardComponent.ets
@Component
export struct CardComponent {
  @BuilderParam header?: () => void
  @BuilderParam content?: () => void

  build() {
    Column() {
      if (this.header) this.header()
      Divider().margin(10)
      if (this.content) this.content()
    }
    .padding(20)
    .backgroundColor('#fff')
    .borderRadius(12)
  }
}
// pages/BuilderParamPage.ets
import { CardComponent } from '../components/CardComponent'

@Entry
@Component
struct BuilderParamPage {
  // ✅ 插槽场景推荐使用 @LocalBuilder
  @LocalBuilder
  headerBuilder() {
    Text('自定义头部').fontSize(22).fontWeight(FontWeight.Bold)
  }

  @LocalBuilder
  contentBuilder() {
    Column({ space: 10 }) {
      Text('插槽内容,灵活传入任意 UI')
      Button('插槽按钮').onClick(() => console.log('点击'))
    }
  }

  build() {
    Column({ space: 20 }) {
      CardComponent({
        header: this.headerBuilder,
        content: this.contentBuilder
      })

      CardComponent({
        header: this.anotherHeader,
        content: this.anotherContent
      })
    }
    .padding(20)
  }

  @LocalBuilder
  anotherHeader() {
    Text('另一个头部').fontSize(20).fontColor('#007DFF')
  }

  @LocalBuilder
  anotherContent() {
    Row({ space: 10 }) {
      Button('确认').backgroundColor('#34C85E')
      Button('取消').backgroundColor('#ff6b35')
    }
  }
}

运行效果
builderParam插槽实战.png

七、总结

7.1 核心对比

对比维度 @Builder @LocalBuilder
定义位置 组件内 / 全局 仅限组件内
this 指向 由调用者决定 永远指向定义组件

7.2 刷新规则完整版

方式 是否刷新
基础类型传参
多参数传值
直接传对象变量
对象字面量
无参数,直接访问状态变量

7.3 选型建议

场景 推荐方案
全局复用,不依赖状态 全局 @Builder
当前组件内部复用 @Builder
作为插槽传给子组件 @LocalBuilder
需要固定 this @LocalBuilder

7.4 避坑清单

  1. ✅ 参数类型必须interface / type
  2. ✅ 响应式刷新:对象字面量传参 无参数直接访问状态变量
  3. ✅ 插槽场景推荐@LocalBuilder
  4. ✅ 箭头函数可以作为插槽传递,this 正确
  5. 禁止使用 Function.bind(编译报错 arkts-no-func-bind)
  6. ❌ 不要直接传 @State 变量给 @Builder(不会刷新)
  7. ❌ 不要在 @Builder 内部修改参数属性(运行时报错)
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐