从 @Builder 到 @LocalBuilder:鸿蒙ArkUI 复用与插槽机制深度解析
·
在 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 刷新。
效果演示

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%')
}
}
效果演示

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%')
}
}
效果演示

五、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。
效果演示

原因: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)
}
}
效果演示

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')
}
}
}
运行效果
七、总结
7.1 核心对比
| 对比维度 | @Builder | @LocalBuilder |
|---|---|---|
| 定义位置 | 组件内 / 全局 | 仅限组件内 |
| this 指向 | 由调用者决定 | 永远指向定义组件 |
7.2 刷新规则完整版
| 方式 | 是否刷新 |
|---|---|
| 基础类型传参 | ❌ |
| 多参数传值 | ❌ |
| 直接传对象变量 | ❌ |
| 对象字面量 | ✅ |
| 无参数,直接访问状态变量 | ✅ |
7.3 选型建议
| 场景 | 推荐方案 |
|---|---|
| 全局复用,不依赖状态 | 全局 @Builder |
| 当前组件内部复用 | @Builder |
| 作为插槽传给子组件 | @LocalBuilder |
| 需要固定 this | @LocalBuilder |
7.4 避坑清单
- ✅ 参数类型必须用
interface/type - ✅ 响应式刷新:对象字面量传参 或 无参数直接访问状态变量
- ✅ 插槽场景推荐用
@LocalBuilder - ✅ 箭头函数可以作为插槽传递,this 正确
- ❌ 禁止使用
Function.bind(编译报错 arkts-no-func-bind) - ❌ 不要直接传
@State变量给@Builder(不会刷新) - ❌ 不要在
@Builder内部修改参数属性(运行时报错)
更多推荐


所有评论(0)