ArkTS 进阶之道(8):@Prop/@Link 父子传值------单向 vs 双向数据流根因
本文是「ArkTS 进阶之道」系列第 8 篇,续「ArkUI 状态哲学」阶段。上篇讲 @State 装饰器赋值就刷 UI 的依赖追踪机制(篇 56)------单组件内部状态。本文讲父子组件传值:@Prop 单向 vs @Link 双向 ------根因在数据流方向绑定机制,@Prop 绑单向「父传子」@Link 绑双向「父子同步」。能力系列篇 14 讲过 @Prop/@Link 怎么用,本文讲为哈单向 vs 双向------根因在数据流绑定。
一、开篇:@Prop/@Link 不是轻量的语法糖,是数据流方向的显式绑定
你写 TypeScript/React 时,父子传值是「魔法」(React props 单向传子,子改要调父 setter):
typescript
// React props 单向 + setter 回调
function Child({ value, onChange }: { value: number; onChange: (v: number) => void }) {
return <Button onClick={() => onChange(value + 1)}>子改 {value}</Button>
}
function Parent() {
const [count, setCount] = useState(0)
return <Child value={count} onChange={setCount} /> ← 父传 value 单向,子改调 onChange 回父改
}
// 子改 value 不刷(单向),子调 onChange 才回父改刷(手动双向)
你写鸿蒙 ArkTS 时,@Prop/@Link 装饰器显式绑数据流方向------不用手写 setter 回调:
typescript
// ArkTS @Prop 单向 + @Link 双向显式绑定
@Component
struct PropChild {
@Prop value: number // @Prop 单向:父传子,子改不影响父
build() { Text(`${this.value}`) }
}
@Component
struct LinkChild {
@Link synced: number // @Link 双向:父子同步变
build() { Text(`${this.synced}`) }
}
@Entry
@Component
struct Index {
@State propValue: number = 1
@State linkValue: number = 1
build() {
Column() {
PropChild({ value: this.propValue }) // 父传 @Prop 单向
LinkChild({ synced: this.linkValue }) // 父传 @Link 双向
}
}
}
魔法 vs 绑定的区别:React 把父子传值当「魔法」(你手写 setter 回调实现双向),ArkTS 把 @Prop/@Link 当「数据流方向绑定」(装饰器编译期绑方向,不用手写 setter)。根因不是魔法是绑定------@Prop 编译期绑单向「父变子变子变父不变」,@Link 编译期绑双向「父子互变同步」。
二、根因:@Prop/@Link 的数据流方向绑定机制
鸿蒙 ArkUI 的 @Prop/@Link 是数据流方向绑定------编译期给字段加方向追踪代码,父变触发子刷 / 子改是否回父,来自三重绑定机制。
机制 1:@Prop 单向绑定------父变子变,子变父不变
@Prop 装饰器编译期绑单向数据流------父 @State 变触发子 @Prop 刷,子 @Prop 改不回父:
typescript
@Component
struct PropChild {
@Prop value: number // @Prop 单向:父变子变,子变父不变
build() {
Column() {
Text(`子 @Prop = ${this.value}`) ← 依赖 @Prop value
Button('子改 @Prop')
.onClick(() => {
this.value = this.value + 100 // 子改 @Prop,只刷自己 UI,父不变
})
}
}
}
@Entry
@Component
struct Index {
@State propValue: number = 1 // 父管 @Prop 的源
build() {
Column() {
Text(`父 @State = ${this.propValue}`)
PropChild({ value: this.propValue }) // 父传 @Prop 单向
Button('父改 @State')
.onClick(() => {
this.propValue++ // 父改 @State,单向传 @Prop 子刷新
})
}
}
}
单向绑定 :@Prop value 编译期绑单向------父 propValue 变触发子 value 刷(父变子变),子 value 改不回父 propValue(子变父不变)。赋值 this.value = this.value + 100 只刷子 UI,父 propValue 仍原值。单向数据流不用手写 setter,装饰器绑方向。
机制 2:@Link 双向绑定------父子互变同步
@Link 装饰器编译期绑双向数据流------父 @State 变触发子 @Link 刷,子 @Link 改也回父 @State:
typescript
@Component
struct LinkChild {
@Link synced: number // @Link 双向:父子互变同步
build() {
Column() {
Text(`子 @Link = ${this.synced}`) ← 依赖 @Link synced
Button('子改 @Link')
.onClick(() => {
this.synced = this.synced + 100 // 子改 @Link,父子同步变(双向)
})
}
}
}
@Entry
@Component
struct Index {
@State linkValue: number = 1 // 父管 @Link 的源
build() {
Column() {
Text(`父 @State = ${this.linkValue}`)
LinkChild({ synced: this.linkValue }) // 父传 @Link 双向
Button('父改 @State')
.onClick(() => {
this.linkValue++ // 父改 @State,双向同步 @Link 子也变
})
}
}
}
双向绑定 :@Link synced 编译期绑双向------父 linkValue 变触发子 synced 刷(父变子变),子 synced 改也回父 linkValue(子变父也变)。赋值 this.synced = this.synced + 100 父子同步变,父子 UI 均刷。双向数据流不用手写 setter 回调,装饰器绑方向。
机制 3:@Prop/@Link 追踪范围不同------单向只追父变 / 双向追父子互变
@Prop/@Link 编译期绑的追踪范围不同------@Prop 只追父变单向通知子,@Link 追父子互变双向同步:
typescript
// @Prop 追踪范围:只追父变单向通知子
// 父 propValue 变 → 通知子 value 刷(父变子变)
// 子 value 改 → 不通知父(子变父不变,单向只追父变方向)
// @Link 追踪范围:追父子互变双向同步
// 父 linkValue 变 → 通知子 synced 刷(父变子变)
// 子 synced 改 → 通知父 linkValue 变(子变父也变,双向追互变方向)
追踪范围区别:@Prop 编译期只追「父变单向通知子」一个方向,@Link 编译期追「父变通知子 + 子变通知父」两个方向。根因不是单向双向的语法区别,是编译期绑的追踪范围不同------@Prop 绑一个方向,@Link 绑两个方向。
三、真机配图:@Prop 单向 vs @Link 双向 数据流对比证据
初始态(父 propValue=1、父 linkValue=1、子 @Prop=1、子 @Link=1 均初始值):

点调子改 @Prop + 子改 @Link + 父改 @State 后(父 propValue=1 不变子 @Prop 变 101 单向、父 linkValue=2 子 @Link 同步变 102 双向均对比证据齐):

对比证据:子改 @Prop 后子变 101 父 propValue 仍 1(单向子变父不变),子改 @Link 后父子同步变(双向互变同步),父改 @State propValue 单向传子刷新,父改 @State linkValue 双向同步子也变。@Prop 单向 vs @Link 双向不是语法区别是编译期绑的数据流追踪范围不同------@Prop 绑一个方向,@Link 绑两个方向。
四、真解法:@Prop/@Link 用法的三个场景
场景 1:@Prop 单向传值(子只读不改父,90% 场景首选)
typescript
@Component
struct DisplayChild {
@Prop title: string // @Prop 单向:子只读显示,不改父
@Prop count: number
build() {
Column() {
Text(this.title) ← 依赖 @Prop title 只读显示
Text(`count = ${this.count}`) ← 依赖 @Prop count 只读显示
}
}
}
@Entry
@Component
struct Index {
@State title: string = '标题'
@State count: number = 0
build() {
Column() {
DisplayChild({ title: this.title, count: this.count }) // 父传 @Prop 单向
Button('父改')
.onClick(() => {
this.count++ // 父改 @State 单向传子刷新
})
}
}
}
为哈能跑 :@Prop 单向绑「父变子变子变父不变」,子只读显示不改父。首选这个,90% 的场景子只读显示用 @Prop 单向就够。要写「子显示父数据不改」时用这个------单向数据流清晰,子改不影响父状态边界。
场景 2:@Link 双向同步(父子互改同步,要双向协作时)
typescript
@Component
struct EditChild {
@Link synced: number // @Link 双向:子可改父也同步变
build() {
Column() {
Text(`同步 = ${this.synced}`)
Button('子改')
.onClick(() => {
this.synced++ // 子改 @Link 父也同步变(双向)
})
}
}
}
@Entry
@Component
struct Index {
@State shared: number = 0 // 父管 @Link 的源
build() {
Column() {
Text(`父 = ${this.shared}`)
EditChild({ synced: this.shared }) // 父传 @Link 双向
Button('父改')
.onClick(() => {
this.shared++ // 父改 @State 双向同步子也变
})
}
}
}
为哈能跑:@Link 双向绑「父子互变同步」,子可改父也同步变。要写「父子双向协作共享状态」时用这个------双向数据流不用手写 setter 回调,装饰器绑两个方向追踪。注意 @Link 的父端必须是 @State(双向同步要父管源状态)。
场景 3:@Prop + @Link 混用(单向显示 + 双向编辑分离)
typescript
@Component
struct MixedChild {
@Prop readonlyTitle: string // @Prop 单向:只读显示标题
@Link editableValue: number // @Link 双向:可编辑值同步
build() {
Column() {
Text(this.readonlyTitle) ← @Prop 只读显示
Text(`值 = ${this.editableValue}`) ← @Link 可编辑同步
Button('子改值')
.onClick(() => {
this.editableValue++ ← 子改 @Link 父也变(双向)
// this.readonlyTitle = '新' ← 子改 @Prop 不影响父(单向,不这么用)
})
}
}
}
@Entry
@Component
struct Index {
@State title: string = '标题'
@State value: number = 0
build() {
Column() {
MixedChild({ readonlyTitle: this.title, editableValue: this.value })
Button('父改标题')
.onClick(() => { this.title = `标题${this.value}` }) ← 父改 @Prop 单向传子
}
}
}
为哈能跑:@Prop + @Link 混用------@Prop 管只读显示字段(单向父传子),@Link 管可编辑字段(双向父子同步)。要写「子组件有只读字段 + 可编辑字段分离」时用这个------单向双向按字段职责分离,数据流边界清晰。
五、一句话哲学
@Prop/@Link 不是语法区别,是编译期绑的数据流方向追踪范围不同。 ArkUI 的 @Prop 单向绑「父变子变子变父不变」一个方向追踪,@Link 双向绑「父子互变同步」两个方向追踪。根因不是单向双向的语法区别,是编译期绑的追踪范围------@Prop 绑一个方向(父变通知子),@Link 绑两个方向(父变通知子 + 子变通知父)。不用手写 setter 回调,装饰器绑方向。
状态哲学阶段串讲:@State 单组件内部(篇 56,赋值就刷 UI 依赖追踪)→ @Prop/@Link 父子传值(篇 57,单向 vs 双向数据流绑定)------从「单组件内部状态」到「父子组件传值」扩展,根因都是编译期绑追踪范围。
下一篇:ArkTS 进阶之道(9)------ @Provide/@Consume 跨层传值:为啥不叫全局变量(对应能力系列篇 15,讲根因)------续「ArkUI 状态哲学」阶段。
能力系列回链
| 能力系列篇 | 本文进阶点 |
|---|---|
| 篇 14 @Prop/@Link 用法 | @Prop/@Link 数据流方向绑定根因 |
| 篇 13 @State 基础用法 | 上一篇:@State 依赖追踪机制 |
| 篇 15 @Provide/@Consume 用法 | 下篇预告:跨层传值为啥不叫全局变量 |
真机 demo 完整代码
typescript
// 篇 57 demo:@Prop 单向 vs @Link 双向 数据流对比
// 显式 interface 声明(见篇 51 装对象字量约束)
interface ICounterObj {
count: number
}
// ✅ @Prop 单向:父传子,子改不影响父
@Component
struct PropChild {
@Prop value: number // @Prop 单向接父值,子改不影响父
build() {
Column({ space: 8 }) {
Text(`@Prop 子接收 = ${this.value}`).fontSize(14).fontColor('#2563eb')
Button('子改 @Prop(只改自己不影响父)')
.width('92%').height(40).fontSize(12)
.onClick(() => {
this.value = this.value + 100 // 子改 @Prop 只刷自己 UI,父不变
})
}
.alignItems(HorizontalAlign.Center)
}
}
// ✅ @Link 双向:父子互改同步
@Component
struct LinkChild {
@Link synced: number // @Link 双向同步,子改父也变
build() {
Column({ space: 8 }) {
Text(`@Link 子同步 = ${this.synced}`).fontSize(14).fontColor('#dc2626')
Button('子改 @Link(父子同步变)')
.width('92%').height(40).fontSize(12)
.onClick(() => {
this.synced = this.synced + 100 // 子改 @Link 父也变(双向同步)
})
}
.alignItems(HorizontalAlign.Center)
}
}
@Entry
@Component
struct Index {
// 父组件 @State
@State propValue: number = 1 // 父管 @Prop 的源
@State linkValue: number = 1 // 父管 @Link 的源
@State log: string = '(未操作)'
build() {
Column({ space: 12 }) {
Text('篇 57 配图:@Prop 单向 vs @Link 双向 数据流')
.fontSize(18).fontWeight(FontWeight.Bold).margin({ top: 20, bottom: 8 })
Text('@Prop 子改不影响父 vs @Link 父子同步变(对比证据)')
.fontSize(12).fontColor('#888').margin({ bottom: 16 })
Column({ space: 6 }) {
Text(`父 @State propValue = ${this.propValue}`).fontSize(15).fontWeight(FontWeight.Bold)
Text(`父 @State linkValue = ${this.linkValue}`).fontSize(15).fontWeight(FontWeight.Bold)
Text(`日志:${this.log}`).fontSize(12).fontColor('#333').margin({ top: 4 })
}
.width('92%').padding(12).backgroundColor('#f5f5f5').borderRadius(8)
// 父传子:@Prop 单向 + @Link 双向
PropChild({ value: this.propValue })
LinkChild({ synced: this.linkValue })
Button('父改 @State propValue(单向传子)')
.width('92%').height(44).fontSize(14)
.onClick(() => {
this.propValue++ // 父改 @State,单向传 @Prop 子刷新
this.log = `父改 propValue=${this.propValue}(单向传子刷,子改不影响父)`
})
Button('父改 @State linkValue(双向同步子)')
.width('92%').height(44).fontSize(14)
.onClick(() => {
this.linkValue++ // 父改 @State,双向同步 @Link 子也变
this.log = `父改 linkValue=${this.linkValue}(双向同步子也变)`
})
}
.width('100%').height('100%').alignItems(HorizontalAlign.Center)
}
}
写鸿蒙 ArkUI 记住 :@Prop 单向绑「父变子变子变父不变」一个方向追踪,@Link 双向绑「父子互变同步」两个方向追踪。根因不是单向双向语法区别是编译期绑的数据流追踪范围------@Prop 绑一个方向(父变通知子),@Link 绑两个方向(父变通知子 + 子变通知父)。@Prop 单向传值子只读不改父(首选),@Link 双向同步父子互改共享状态(要双向协作),@Prop + @Link 混用按字段职责分离单向双向。不用手写 setter 回调,装饰器绑方向是 ArkUI 状态哲学核心!