概述

把一段重复的界面抽成 @Builder自定义构建函数,是 ArkUI 里最常用的复用手段之一。但很多人抽完之后会撞上一个奇怪的现象:某个 @Builder里展示的文字,在别处把对应的数据改了之后,这里就是不刷新——日志里数据变了,界面纹丝不动。

这通常不是 @Builder写错了,而是传参方式用错了。@Builder的参数传递分两种:默认的"按值传递"和需要用 `$$` 语法声明的"按引用传递"。前者只把调用那一刻的值拍个快照交给 UI,源数据之后怎么变都跟它无关;后者才会让 UI 与源数据建立依赖,数据一变、界面跟着变。搞不清这两者的区别,就会出现"抽得越多、bug 越隐蔽"的局面。本文先讲清楚两种传参各自的机制和适用场景,再用一个领券减价的小场景演示正确的写法。

 

说明:值传递与引用传递

按值传递(默认)

@Builder的形参如果是普通类型参数,调用时就是按值传递:

typescript
@Builder
priceView(price: number) { ... }
 

调用 `this.priceView(this.price)` 时,price拿到的是当前值的拷贝。此后无论 this.price怎么变,这段 @Builder里用到的都还是当时那个旧值,UI 自然不刷新。它的好处是无依赖、隔离性好,适合展示相对静态、不随状态联动的内容;坏处是"改不动"。

按引用传递(`$$`)

当希望 UI 跟源数据联动时,把形参声明成$$ 开头的对象字面量:

@Builder
priceView($$: { price: number }) { ... }

调用时传 `this.priceView({ price: this.price })`。注意 `$$` 大括号里写的是**当前组件成员变量的名字**,UI 框架会据此建立"数据 → 这段 UI"的依赖关系:只要 `this.price` 变化,对应位置自动刷新。这正是"数据驱动 UI"在 `@Builder` 里的完整形态。

两条使用规则记住即可:一,`@Builder` 内部不允许定义状态变量,它只负责拼装 UI,状态仍由宿主组件持有;二,引用传递的 `$$` 对象里,值的位置要放组件的成员变量(尤其是 `@State` 等状态变量),随意塞一个临时对象或常量是建立不起依赖的。如果只是想在页面某个位置"留个口子"由父组件注入一段 UI,那是 `@BuilderParam` 的职责。

 

使用实践:领券减价不生效的修复

场景:商品页显示现价和原价,用户点"领券"按钮后价格立减 50。最初价格展示被抽成了 `@Builder`,但用的是按值传递——`this.priceView(this.price)`——所以点击按钮后,接口返回的新价格已经写进 `this.price`,界面上的价格却一动不动。正确的做法是把形参改成 `$$` 引用传递,让价格展示与 `this.price` 建立依赖:

@Entry
@Component
struct GoodsPage {
  @State price: number = 199;
  @State originalPrice: number = 299;

  @Builder
  priceView($$: { price: number, originalPrice: number }) {
    Column() {
      Text(`${$$.price} 元`).fontSize(28).fontWeight(FontWeight.Bold)
      Text(`原价 ${$$.originalPrice} 元`).fontSize(14).fontColor('#999999')
    }
  }

  build() {
    Column({ space: 16 }) {
      this.priceView({ price: this.price, originalPrice: this.originalPrice })

      Button('领券立减50')
        .onClick(() => {
          this.price = this.originalPrice - 50;
        })
    }
  }
}

对应到刚才的两个概念,可以这样对照理解:

1. 差异就在形参那一行。把 `priceView(price: number)` 改成 `priceView($$: { price: number, originalPrice: number })`,再把调用改成对象字面量形式,这段 `@Builder` 就从"快照"变成了"活数据"。按钮回调里给 `this.price` 赋新值,价格文本立刻联动刷新。
2. 对象字面量的键名要跟形参对应,值要写成员变量。** `{ price: this.price, originalPrice: this.originalPrice }` 里,键是形参名,值是从组件成员变量取引用的位置,两者不能写反,也不能写死成常量。
3. 如果某个值确实不需要联动,再退回按值传递。比如原价基本不变,单独一个展示位用值传递没问题;但像价格这样会被业务逻辑频繁改动的,必须走引用传递。

这样改完之后,价格展示就真正"活"了起来:领券、改价、回退,`@Builder` 里的每一处文本都会跟着数据同步更新,不再出现"数据变了 UI 没变"的诡异现象。抽组件之前,先想清楚这个片段要不要跟随状态刷新,就能提前避开这个坑。

Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐