列表项不刷新,这个话题在很多鸿蒙开发群里都能炸出几个人来。尤其是用了@Builder装饰器之后,数据源明明已经更新,界面却像被按了暂停键一样纹丝不动。其实这不怪ArkUI,也不怪你写的业务代码,真正要背锅的往往是@Builder的参数传递机制在起作用。今天这篇文章,我就把值传递和引用传递这件事一次性讲透,从原理到实战,把你在开发中可能踩过的坑都摊开来说。
先用一句话交代清楚:@Builder是ArkUI提供的自定义构建函数装饰器,它可以把一段UI结构封装成函数,在多个地方复用。它的参数传递分为按值传递和按引用传递两种方式,理解它们之间的差别,是你写出界面联动正确、逻辑不绕弯的鸿蒙应用必须跨过的一道门槛。这篇内容适合已经掌握ArkTS基础语法、正在进阶组件封装和状态管理的开发者,读完你能直接拿来排查自己项目里的类似问题。
1. 先搞懂 @Builder 是干嘛的:它替你省掉了什么
1.1 为什么需要 @Builder:一段 UI 写三遍之后的觉悟
在没有@Builder之前,如果你要在三个页面里展示同一种商品卡片的布局,你最直接的做法就是Ctrl+C、Ctrl+V,把那段Row、Column、Image、Text的组合从A页面复制到B页面再复制到C页面。写的时候确实痛快,但等到产品经理说要调整卡片的圆角、字号和间距时,你就得满项目地找这些重复代码,一处漏改就是一次线上事故。这种痛,经历过的人都懂。
@Builder解决的就是这个问题。它让你把一段UI描述包装成一个构建函数,需要时像一个普通函数一样调用,函数放在组件内部就是局部@Builder,放在组件外部就是全局@Builder。它做的本质上是“UI结构的代码级复用”,不是组件级复用。这意味着它不会创建一个新的组件实例,也不会有额外的组件通信开销,只是把这段UI描述“原地展开”。这个定位决定了它的使用方式,也决定了它在参数处理上的特殊性。
我打个比方:@Builder就像你做菜时写好的“配菜清单”,每次炒同一道菜时照着清单把材料摆好,而不是重新雇一个厨师(自定义组件)来开火。厨师的工资更高,但如果你只是想复用摆盘的方式,那清单就够用了。理解了这一点,你就明白为什么很多列表项、卡片标题、标签按钮都适合用@Builder封装,而不是非得拆成一个@Component子组件。
1.2 局部 @Builder 与全局 @Builder:访问 this 的边界
@Builder有两种定义位置,使用场景和约束完全不同。局部@Builder定义在组件内部,通过this调用,最大的好处是函数体内部可以直接访问组件的状态变量和方法。比如你可以在组件内写一个@Builder Card(),里面直接用this.title、this.desc来渲染,非常自然。
全局@Builder定义在组件外部,和普通顶层函数一样,它没有this,也拿不到任何组件的成员。它只能依赖通过参数传入的数据。这个约束很多人会忽略,结果在全局@Builder里尝试访问某个状态变量时直接编译报错,或者拿到的值是undefined,一头雾水。全局@Builder适合封装那种完全由数据驱动、不依赖页面上下文的UI片段,比如通用空状态、统一格式的标签、通用列表行。
typescript复制// 组件外定义全局 @Builder
@Builder function GlobalTag(text: string) {
Text(text)
.fontSize(12)
.fontColor('#FFFFFF')
.backgroundColor('#E84026')
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
}
局部@Builder的写法只是在组件结构体内部,注意调用时用this前缀,否则编译器找不到:
typescript复制@Component
struct DemoPage {
@State count: number = 0
@Builder LocalCard() {
Column() {
Text(`当前数量:${this.count}`)
}
}
build() {
Column() {
this.LocalCard()
}
}
}
从长期的开发体验来看,我建议遵循一个原则:如果这个UI片段只在当前组件里用,就用局部@Builder;如果多个页面、多个组件都要用,而且靠参数就能画出来,就抽成全局@Builder。不要在局部@Builder里写太多和组件状态强耦合的逻辑,否则调用方想复用时会发现搬不动。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心机制拆解:@Builder 的值传递和引用传递到底差在哪
2.1 值传递:靠参数快照渲染,不跟外部联动
所谓按值传递,就是你直接把一个变量、一个对象或一个数组作为参数传给@Builder函数。这个阶段的函数会拿你传入的值去做一次UI渲染,但不会和外部数据建立长期的联动依赖。翻译成大白话说就是:它只记住了你第一次给它的值,后面外部怎么变,它一概不管。
很多人的第一反应是:我传的不是一个对象吗?对象在JavaScript里不是引用类型吗?传引用应该会跟随变化啊。这个说法在普通编程里是对的,但在ArkUI的UI渲染体系里,“值传递”和“引用传递”的定义并不仅仅依赖数据类型,而是依赖ArkUI是否为目标参数建立了状态依赖。按值传递时,@Builder内部虽然拿到了对象的引用地址,但它没有登记“我要监听这个对象的哪些属性”,所以当属性变化时,UI刷新机制根本不会通知它。
我们来看一段典型的踩坑代码:
typescript复制@Builder function ValueCounter(count: number) {
Text(`当前数量:${count}`)
}
@Entry
@Component
struct ValueDemo {
@State count: number = 0
build() {
Column({ space: 12 }) {
ValueCounter(this.count)
Button('点击增加')
.onClick(() => {
this.count++
})
}
}
}
运行起来你会发现,每次点击按钮,this.count确实在增加,但ValueCounter里显示的文本永远都是0。这不是Bug,而是@Builder按值传递的预期表现。外层状态变化时,@Builder不会重新渲染。你也可以在@Bui
