ArkTS 进阶之道(8):@Prop/@Link 父子传值——单向 vs 双向数据流根因

📅 2026/7/29 1:52:51 👁️ 阅读次数 📝 编程学习
ArkTS 进阶之道(8):@Prop/@Link 父子传值——单向 vs 双向数据流根因

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):

// 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 回调:

// 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 改不回父:

@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 子刷新 }) } } }

单向绑定:@Propvalue编译期绑单向——父propValue变触发子value刷(父变子变),子value改不回父propValue(子变父不变)。赋值this.value = this.value + 100只刷子 UI,父propValue仍原值。单向数据流不用手写 setter,装饰器绑方向。

机制 2:@Link 双向绑定——父子互变同步

@Link 装饰器编译期绑双向数据流——父 @State 变触发子 @Link 刷,子 @Link 改也回父 @State:

@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 子也变 }) } } }

双向绑定:@Linksynced编译期绑双向——父linkValue变触发子synced刷(父变子变),子synced改也回父linkValue(子变父也变)。赋值this.synced = this.synced + 100父子同步变,父子 UI 均刷。双向数据流不用手写 setter 回调,装饰器绑方向。

机制 3:@Prop/@Link 追踪范围不同——单向只追父变 / 双向追父子互变

@Prop/@Link 编译期绑的追踪范围不同——@Prop 只追父变单向通知子,@Link 追父子互变双向同步:

// @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% 场景首选)

@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 双向同步(父子互改同步,要双向协作时)

@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 混用(单向显示 + 双向编辑分离)

@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 完整代码

// 篇 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 状态哲学核心!