ArkTS 中 @State、@Prop 和 @Link 的区别总结

📌 文档说明

本文档详细对比 ArkTS 中三个核心状态管理装饰器:@State@Prop@Link 的区别、使用场景和注意事项。适合用于:

  • 理解 ArkTS 组件间通信机制
  • 面试准备和技术交流
  • 选择合适的状态管理方式

🎯 一、快速对比表

特性 @State @Prop @Link
作用域 组件内部 父 → 子 父 ↔ 子
数据流 私有状态 单向(只读) 双向(可读可写)
可修改 ✅ 可以 ❌ 不可以 ✅ 可以
影响范围 当前组件 仅子组件 父子同步
传递方式 - 直接传值 使用 $ 传引用
使用场景 组件私有状态 展示型组件 表单、交互组件
初始化 必须赋初值 从父组件接收 从父组件接收
响应式 ✅ 响应式 ✅ 响应式 ✅ 响应式

💡 二、核心概念

记忆口诀

@State  →  我的数据我做主(私有状态)
@Prop   →  只能看不能改(单向传递)
@Link   →  大家一起用(双向绑定)

数据流向图

┌─────────────┐
│   @State    │  组件内部独享
│  (私有状态)  │  ↓ 修改
│             │  ↓ 触发 UI 更新
└─────────────┘

┌─────────────┐
│ 父组件       │
│ @State data │
│             │
│      ↓ (传值)
│
│ 子组件       │
│ @Prop data  │  ← 只读,不能修改
│             │
└─────────────┘

┌─────────────┐
│ 父组件       │
│ @State data │
│      ↕ (传引用 $)
│
│ 子组件       │
│ @Link data  │  ← 可读可写,父子同步
│             │
└─────────────┘

📖 三、详细讲解

3.1 @State - 组件私有状态

定义

@State 装饰的变量是组件的私有状态,只在当前组件内部使用,修改会触发组件 UI 更新。

特点
  • 组件私有:不能从外部传入
  • 响应式更新:修改自动触发 UI 重新渲染
  • 必须初始化:声明时必须赋初值
  • ⚠️ 作用域限制:只影响当前组件
基本用法
@Entry
@Component
struct CounterDemo {
  @State count: number = 0  // ✅ 必须赋初值
  @State message: string = 'Hello'
  @State isVisible: boolean = true

  build() {
    Column({ space: 10 }) {
      Text(`计数: ${this.count}`)
      Text(this.message)

      Button('增加')
        .onClick(() => {
          this.count++  // ✅ 可以修改
          this.message = 'World'
        })

      Button('切换显示')
        .onClick(() => {
          this.isVisible = !this.isVisible
        })

      if (this.isVisible) {
        Text('显示内容')
      }
    }
  }
}
使用场景

适合的场景

  • ✅ 组件内部的计数器、索引
  • ✅ 开关状态(visible、loading、checked)
  • ✅ 表单输入值(暂存在组件内)
  • ✅ 列表数据(组件独有的数据)
  • ✅ 临时状态(弹窗、Toast)

代码示例

@Entry
@Component
struct TodoList {
  @State todos: string[] = []  // 待办列表
  @State inputText: string = ''  // 输入框内容
  @State loading: boolean = false  // 加载状态

  build() {
    Column() {
      // 输入框
      TextInput({ text: this.inputText })
        .onChange((value: string) => {
          this.inputText = value
        })

      // 添加按钮
      Button('添加')
        .onClick(() => {
          if (this.inputText) {
            this.todos.push(this.inputText)
            this.inputText = ''
          }
        })

      // 列表
      if (this.loading) {
        LoadingProgress()
      } else {
        List() {
          ForEach(this.todos, (todo: string) => {
            ListItem() {
              Text(todo)
            }
          })
        }
      }
    }
  }
}

3.2 @Prop - 父向子单向传递

定义

@Prop 实现父组件向子组件的单向数据传递,子组件只能读取不能修改,父组件数据变化时子组件自动更新。

特点
  • 单向数据流:父 → 子
  • 只读属性:子组件不能修改
  • 自动更新:父组件变化,子组件自动更新
  • 传值方式:直接传值,不需要 $
  • ⚠️ 修改报错:尝试修改会编译错误
基本用法
// ========== 子组件 ==========
@Component
struct UserCard {
  @Prop username: string  // 从父组件接收
  @Prop age: number
  @Prop avatar: string

  build() {
    Column({ space: 10 }) {
      Image(this.avatar)
        .width(80)
        .height(80)
        .borderRadius(40)

      Text(this.username)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)

      Text(`年龄: ${this.age}`)
        .fontSize(14)
        .fontColor(Color.Gray)

      Button('尝试修改')
        .onClick(() => {
          // ❌ 错误:不能修改 @Prop
          // this.username = 'New Name'  // 编译错误!
          console.log('子组件不能修改 @Prop')
        })
    }
    .padding(20)
    .backgroundColor(Color.White)
    .borderRadius(12)
  }
}

// ========== 父组件 ==========
@Entry
@Component
struct UserProfile {
  @State userName: string = 'Tom'
  @State userAge: number = 20
  @State userAvatar: string = ''

  build() {
    Column({ space: 20 }) {
      Text('用户资料')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)

      // 传递给子组件(直接传值)
      UserCard({
        username: this.userName,  // ✅ 直接传值
        age: this.userAge,
        avatar: this.userAvatar
      })

      Button('修改用户信息')
        .onClick(() => {
          this.userName = 'Jerry'  // ✅ 父组件修改
          this.userAge = 25
          // 子组件自动更新
        })
    }
    .width('100%')
    .padding(20)
  }
}
使用场景

适合的场景

  • ✅ 展示型组件(卡片、列表项、头像)
  • ✅ 只读数据(商品信息、用户资料、文章详情)
  • ✅ 静态配置(主题色、字体大小、样式参数)
  • ✅ 纯 UI 组件(按钮、标签、徽章)

完整示例:商品卡片

// 商品数据接口
interface Product {
  id: string
  name: string
  price: number
  image: string
  stock: number
}

// ========== 子组件:商品卡片 ==========
@Component
struct ProductCard {
  @Prop productName: string
  @Prop price: number
  @Prop imageUrl: string
  @Prop stock: number

  build() {
    Column({ space: 10 }) {
      // 商品图片
      Image(this.imageUrl)
        .width('100%')
        .height(200)
        .borderRadius(8)
        .backgroundColor('#F5F5F5')

      // 商品名称
      Text(this.productName)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .maxLines(2)
        .textOverflow({ overflow: TextOverflow.Ellipsis })

      // 价格和库存
      Row() {
        Text(`¥${this.price}`)
          .fontSize(20)
          .fontColor('#FF4D4F')
          .fontWeight(FontWeight.Bold)

        Spacer()

        Text(`库存: ${this.stock}`)
          .fontSize(14)
          .fontColor(Color.Gray)
      }
      .width('100%')

      // 购买按钮
      Button('立即购买')
        .width('100%')
        .backgroundColor('#FF4D4F')
    }
    .width('100%')
    .padding(15)
    .backgroundColor(Color.White)
    .borderRadius(12)
  }
}

// ========== 父组件:商品列表 ==========
@Entry
@Component
struct ProductList {
  @State products: Product[] = [
    { id: '1', name: 'iPhone 15 Pro', price: 7999, stock: 100, image: '' },
    { id: '2', name: 'iPad Pro', price: 6799, stock: 50, image: '' },
    { id: '3', name: 'MacBook Pro', price: 12999, stock: 30, image: '' }
  ]

  build() {
    Scroll() {
      Column({ space: 15 }) {
        Text('商品列表')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)

        // 使用 ForEach 渲染商品列表
        ForEach(this.products, (product: Product) => {
          ProductCard({
            productName: product.name,
            price: product.price,
            imageUrl: product.image,
            stock: product.stock
          })
        }, (product: Product) => product.id)
      }
      .width('100%')
      .padding(15)
    }
  }
}

3.3 @Link - 父子双向同步

定义

@Link 实现父子组件的双向数据绑定,子组件可以修改数据,修改会同步到父组件。

特点
  • 双向绑定:父 ↔ 子,数据同步
  • 子组件可写:子组件可以修改数据
  • 父子同步:任一方修改,另一方自动更新
  • ⚠️ 传引用:必须使用 $ 符号传递引用
  • ⚠️ 不能初始化:不能在子组件中赋初值
基本用法
// ========== 子组件 ==========
@Component
struct Counter {
  @Link count: number  // ✅ 双向绑定

  build() {
    Column({ space: 10 }) {
      Text('子组件')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)

      Text(`计数: ${this.count}`)
        .fontSize(24)

      Button('子组件增加')
        .onClick(() => {
          this.count++  // ✅ 可以修改,父组件会同步
        })

      Button('子组件减少')
        .onClick(() => {
          this.count--
        })
    }
    .padding(20)
    .backgroundColor('#E3F2FD')
    .borderRadius(12)
  }
}

// ========== 父组件 ==========
@Entry
@Component
struct ParentPage {
  @State parentCount: number = 0  // 父组件状态

  build() {
    Column({ space: 20 }) {
      Text('父组件')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)

      Text(`父组件看到的计数: ${this.parentCount}`)
        .fontSize(16)

      // 使用 $ 传递引用
      Counter({ count: $parentCount })  // ⚠️ 注意 $ 符号

      Button('父组件增加')
        .onClick(() => {
          this.parentCount += 10  // ✅ 父组件修改,子组件同步
        })

      Button('父组件重置')
        .onClick(() => {
          this.parentCount = 0
        })
    }
    .width('100%')
    .padding(20)
  }
}
使用场景

适合的场景

  • ✅ 表单输入组件(TextInput、DatePicker)
  • ✅ 开关切换(Toggle、Checkbox、Radio)
  • ✅ 可编辑列表(增删改查)
  • ✅ 双向绑定控件(Slider、Rating)
  • ✅ 需要父子协作的组件

完整示例:表单组件

// ========== 子组件:输入框 ==========
@Component
struct FormInput {
  @Link value: string  // 双向绑定
  private label: string = ''
  private placeholder: string = ''

  build() {
    Column({ space: 5 }) {
      Text(this.label)
        .fontSize(14)
        .fontWeight(FontWeight.Medium)

      TextInput({ text: this.value, placeholder: this.placeholder })
        .onChange((text: string) => {
          this.value = text  // ✅ 子组件修改,父组件同步
        })
        .width('100%')
        .height(40)
    }
    .alignItems(HorizontalAlign.Start)
    .width('100%')
  }
}

// ========== 子组件:开关 ==========
@Component
struct FormSwitch {
  @Link checked: boolean  // 双向绑定
  private label: string = ''

  build() {
    Row() {
      Text(this.label)
        .fontSize(14)
        .layoutWeight(1)

      Toggle({ type: ToggleType.Switch, isOn: this.checked })
        .onChange((isOn: boolean) => {
          this.checked = isOn  // ✅ 子组件修改,父组件同步
        })
    }
    .width('100%')
    .padding(10)
  }
}

// ========== 父组件:注册表单 ==========
@Entry
@Component
struct RegistrationForm {
  @State username: string = ''
  @State email: string = ''
  @State password: string = ''
  @State agreeTerms: boolean = false

  // 提交表单
  handleSubmit() {
    if (!this.username || !this.email || !this.password) {
      AlertDialog.show({
        message: '请填写完整信息',
        confirm: { value: '确定' }
      })
      return
    }

    if (!this.agreeTerms) {
      AlertDialog.show({
        message: '请同意服务条款',
        confirm: { value: '确定' }
      })
      return
    }

    console.log('提交数据:', {
      username: this.username,
      email: this.email,
      password: this.password
    })

    AlertDialog.show({
      message: '注册成功!',
      confirm: { value: '确定' }
    })
  }

  build() {
    Scroll() {
      Column({ space: 20 }) {
        Text('用户注册')
          .fontSize(28)
          .fontWeight(FontWeight.Bold)

        // 使用 @Link 组件
        FormInput({
          value: $username,  // ⚠️ 使用 $ 传引用
          label: '用户名',
          placeholder: '请输入用户名'
        })

        FormInput({
          value: $email,
          label: '邮箱',
          placeholder: '请输入邮箱'
        })

        FormInput({
          value: $password,
          label: '密码',
          placeholder: '请输入密码'
        })

        FormSwitch({
          checked: $agreeTerms,
          label: '我已阅读并同意服务条款'
        })

        // 显示表单数据(父组件可以看到子组件的修改)
        Column({ space: 5 }) {
          Text('表单数据预览:')
            .fontSize(14)
            .fontColor(Color.Gray)

          Text(`用户名: ${this.username}`)
          Text(`邮箱: ${this.email}`)
          Text(`密码: ${'*'.repeat(this.password.length)}`)
          Text(`同意条款: ${this.agreeTerms ? '是' : '否'}`)
        }
        .alignItems(HorizontalAlign.Start)
        .width('100%')
        .padding(10)
        .backgroundColor('#F5F5F5')
        .borderRadius(8)

        Button('注册')
          .width('100%')
          .height(44)
          .fontSize(16)
          .backgroundColor('#007DFF')
          .onClick(() => this.handleSubmit())
      }
      .width('100%')
      .padding(20)
    }
  }
}

🔄 四、三者对比示例

让我们通过一个完整的示例来看三者的区别:

// ========== 子组件定义 ==========

// 1. 使用 @Prop(只读)
@Component
struct DisplayCard {
  @Prop title: string
  @Prop count: number

  build() {
    Column({ space: 10 }) {
      Text(this.title)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)

      Text(`${this.count}`)
        .fontSize(24)
        .fontColor('#007DFF')

      Button('子组件尝试修改')
        .onClick(() => {
          // ❌ 不能修改 @Prop
          // this.count++  // 编译错误
          console.log('不能修改 @Prop')
        })
    }
    .width('100%')
    .padding(15)
    .backgroundColor('#F0F0F0')
    .borderRadius(8)
  }
}

// 2. 使用 @Link(可读可写)
@Component
struct EditableCard {
  @Link count: number  // 双向绑定

  build() {
    Column({ space: 10 }) {
      Text('可编辑卡片')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)

      Text(`${this.count}`)
        .fontSize(24)
        .fontColor('#FF4D4F')

      Button('子组件增加')
        .onClick(() => {
          this.count++  // ✅ 可以修改,父组件同步
        })

      Button('子组件减少')
        .onClick(() => {
          this.count--
        })
    }
    .width('100%')
    .padding(15)
    .backgroundColor('#E3F2FD')
    .borderRadius(8)
  }
}

// ========== 父组件 ==========

@Entry
@Component
struct ComparisonDemo {
  @State privateCount: number = 0      // ① 组件私有状态
  @State propCount: number = 0         // ② 用于 @Prop 的状态
  @State linkCount: number = 0         // ③ 用于 @Link 的共享状态

  build() {
    Scroll() {
      Column({ space: 30 }) {
        Text('@State、@Prop、@Link 对比')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)

        Divider()

        // ========== @State 示例 ==========
        Column({ space: 10 }) {
          Text('1️⃣ @State - 组件私有状态')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FF6B6B')

          Text(`私有计数: ${this.privateCount}`)
            .fontSize(16)

          Button('修改私有状态')
            .onClick(() => {
              this.privateCount++  // ✅ 只影响当前组件
            })

          Text('特点:组件内部独享,修改只影响当前组件')
            .fontSize(12)
            .fontColor(Color.Gray)
        }
        .alignItems(HorizontalAlign.Start)
        .width('100%')
        .padding(15)
        .backgroundColor('#FFE5E5')
        .borderRadius(12)

        Divider()

        // ========== @Prop 示例 ==========
        Column({ space: 10 }) {
          Text('2️⃣ @Prop - 单向传递(父 → 子)')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#4ECDC4')

          Text(`父组件的值: ${this.propCount}`)
            .fontSize(16)

          // 子组件(@Prop)
          DisplayCard({
            title: '只读卡片',
            count: this.propCount  // ✅ 直接传值
          })

          Button('父组件修改')
            .onClick(() => {
              this.propCount++  // 父组件修改,子组件自动更新
            })

          Text('特点:子组件只读,父组件修改会同步')
            .fontSize(12)
            .fontColor(Color.Gray)
        }
        .alignItems(HorizontalAlign.Start)
        .width('100%')
        .padding(15)
        .backgroundColor('#E0F7F6')
        .borderRadius(12)

        Divider()

        // ========== @Link 示例 ==========
        Column({ space: 10 }) {
          Text('3️⃣ @Link - 双向绑定(父 ↔ 子)')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#95E1D3')

          Text(`父组件看到的值: ${this.linkCount}`)
            .fontSize(16)

          // 子组件(@Link)
          EditableCard({
            count: $linkCount  // ⚠️ 使用 $ 传引用
          })

          Button('父组件修改')
            .onClick(() => {
              this.linkCount += 10  // 父子同步
            })

          Button('父组件重置')
            .onClick(() => {
              this.linkCount = 0
            })

          Text('特点:子组件可修改,父子双向同步')
            .fontSize(12)
            .fontColor(Color.Gray)
        }
        .alignItems(HorizontalAlign.Start)
        .width('100%')
        .padding(15)
        .backgroundColor('#E8F8F5')
        .borderRadius(12)

        Divider()

        // ========== 总结 ==========
        Column({ space: 10 }) {
          Text('📝 总结')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)

          Text('• @State: 我的数据我做主(私有状态)')
            .fontSize(14)
          Text('• @Prop: 只能看不能改(单向传递)')
            .fontSize(14)
          Text('• @Link: 大家一起用(双向绑定)')
            .fontSize(14)
        }
        .alignItems(HorizontalAlign.Start)
        .width('100%')
        .padding(15)
        .backgroundColor('#FFF9E6')
        .borderRadius(12)
      }
      .width('100%')
      .padding(20)
    }
  }
}

⚠️ 五、常见错误与注意事项

5.1 错误 1:@Prop 尝试修改

@Component
struct Child {
  @Prop value: string

  build() {
    Button('修改')
      .onClick(() => {
        // ❌ 编译错误!@Prop 是只读的
        this.value = 'new value'
      })
  }
}

错误信息

Property 'value' is read-only

正确做法

如果需要修改,应该使用 @Link 或通过回调函数通知父组件:

@Component
struct Child {
  @Prop value: string
  private onChange?: (newValue: string) => void  // 回调函数

  build() {
    Button('修改')
      .onClick(() => {
        // ✅ 通过回调通知父组件
        if (this.onChange) {
          this.onChange('new value')
        }
      })
  }
}

@Entry
@Component
struct Parent {
  @State parentValue: string = 'old'

  build() {
    Child({
      value: this.parentValue,
      onChange: (newValue: string) => {
        this.parentValue = newValue  // 父组件修改
      }
    })
  }
}

5.2 错误 2:@Link 忘记使用 $

@Entry
@Component
struct Parent {
  @State count: number = 0

  build() {
    Column() {
      // ❌ 错误:直接传值
      Child({ count: this.count })

      // ✅ 正确:使用 $ 传引用
      Child({ count: $count })
    }
  }
}

错误现象

  • 子组件修改不会同步到父组件
  • 父组件修改也不会同步到子组件
  • 失去了双向绑定的效果

记忆技巧

// @Prop:直接传值(单向)
Child({ value: this.data });

// @Link:使用 $ 传引用(双向)
Child({ value: $data }); // $ = 引用

5.3 错误 3:混淆使用场景

// ❌ 错误:展示型组件不需要 @Link
@Component
struct ProductCard {
  @Link productName: string  // 只是展示,不需要双向绑定
  @Link price: number
}

// ✅ 正确:展示型组件用 @Prop
@Component
struct ProductCard {
  @Prop productName: string
  @Prop price: number
}

// ❌ 错误:表单组件用 @Prop
@Component
struct InputField {
  @Prop value: string  // 用户输入无法同步到父组件
}

// ✅ 正确:表单组件用 @Link
@Component
struct InputField {
  @Link value: string  // 用户输入可以同步到父组件
}

5.4 错误 4:@State 初始化问题

@Component
struct Demo {
  // ❌ 错误:@State 必须初始化
  @State count: number

  // ✅ 正确:必须赋初值
  @State count: number = 0
}

5.5 错误 5:@Link 不能初始化

@Component
struct Child {
  // ❌ 错误:@Link 不能赋初值
  @Link count: number = 0

  // ✅ 正确:从父组件接收
  @Link count: number
}

📊 六、选择指南

6.1 决策树

需要状态管理?
  ├─ 是否需要从父组件接收?
  │   ├─ 否 → 使用 @State
  │   │      (组件私有状态)
  │   │
  │   └─ 是 → 子组件是否需要修改?
  │          ├─ 否 → 使用 @Prop
  │          │      (只读,展示型组件)
  │          │
  │          └─ 是 → 使用 @Link
  │                 (可写,表单组件)
  │
  └─ 否 → 使用普通变量

6.2 使用场景对照表

场景 装饰器 说明
计数器(组件内部) @State 组件私有状态
Loading 状态 @State 组件控制
弹窗显示/隐藏 @State 组件内部控制
商品卡片展示 @Prop 只读,展示商品信息
用户头像组件 @Prop 只读,展示用户信息
文章列表项 @Prop 只读,展示文章信息
TextInput 输入框 @Link 需要同步输入值
Checkbox 复选框 @Link 需要同步选中状态
Toggle 开关 @Link 需要同步开关状态
可编辑列表 @Link 需要同步编辑结果
Slider 滑块 @Link 需要同步滑块值

🎤 七、面试回答模板

7.1 基础版本(30 秒)

"@State、@Prop 和 @Link 是 ArkTS 中三种不同的状态管理装饰器

  • @State 是组件的私有状态,只在当前组件内使用,修改会触发 UI 更新
  • @Prop 实现父向子单向传递,子组件只能读取不能修改,父组件变化时子组件自动更新
  • @Link 实现父子双向绑定,子组件修改会同步到父组件,需要用 $ 符号传递引用

简单来说:@State 是自己用,@Prop 是看别人的,@Link 是和别人共享。"

7.2 进阶版本(1-2 分钟)

"从数据流向来看:

@State 是组件的内部状态,完全由组件自己控制,比如一个计数器的 count 值。修改它只会影响当前组件的 UI。

@Prop 实现单向数据流:父 → 子。它像一个只读属性,子组件接收父组件传来的值,可以使用但不能修改。如果父组件的值变了,子组件会自动更新。比如商品卡片组件接收商品信息,只用来展示。传递时直接传值:Child({ value: this.data })

@Link 实现双向绑定:父 ↔ 子。子组件可以修改这个值,修改后会同步到父组件。传递时需要用 $ 符号传引用而不是传值:Child({ value: $data })。比如表单输入组件,子组件的输入要同步给父组件。

使用场景上

  • 组件内部的临时状态用 @State
  • 展示型组件用 @Prop
  • 表单、可编辑组件用 @Link"

7.3 深度版本(追问)

如果问:为什么 @Link 需要 $ 符号?

"$ 符号表示传递的是引用而不是值

在 JavaScript 中,基本类型(number、string、boolean)是值传递,对象是引用传递。但在 ArkTS 中,为了明确表示’这是一个需要双向绑定的引用’,使用 $ 符号来标记。

当你使用 $data 时,传递的是 data 的引用,子组件和父组件指向同一个数据源。子组件修改时,实际上是修改了共享的数据,所以父组件也能看到变化。

如果不用 $,传递的就是值的拷贝,子组件修改只是修改自己的拷贝,不会影响父组件。"

如果问:@Prop 为什么是只读的?

"这是为了保持单向数据流的设计原则

单向数据流的好处是:

  1. 数据流向清晰:数据总是从父组件流向子组件,便于追踪
  2. 避免混乱:如果子组件能随意修改,数据来源就不明确了
  3. 便于调试:出问题时只需要检查父组件,不用担心子组件偷偷修改

如果确实需要子组件修改数据,有两种方式:

  1. 使用 @Link 实现双向绑定
  2. 通过回调函数通知父组件修改,保持单向数据流

这种设计在 React、Vue 等主流框架中也是一样的。"


🎯 八、核心要点总结

8.1 记忆口诀

@State  →  我的数据我做主(私有状态)
@Prop   →  只能看不能改(单向传递)
@Link   →  大家一起用(双向绑定)

8.2 传递方式

// @State:不需要传递
@State data: string = 'value'

// @Prop:直接传值
Child({ data: this.data })

// @Link:使用 $ 传引用
Child({ data: $data })  // 注意 $ 符号

8.3 使用决策

第一步:问自己"数据从哪来?"

  • 组件内部产生 → @State
  • 父组件传入 → 继续第二步

第二步:问自己"需要修改吗?"

  • 不需要修改(只展示) → @Prop
  • 需要修改(可编辑) → @Link

8.4 对比总结

装饰器 数据流 可修改 传递方式 典型场景
@State 组件内部 ✅ 是 - 计数器、状态
@Prop 父 → 子 ❌ 否 直接传值 商品卡片、头像
@Link 父 ↔ 子 ✅ 是 $ 传引用 表单、开关

📚 九、最佳实践

9.1 优先使用 @State + @Prop

原则:优先使用单向数据流(@State + @Prop),只在必要时使用 @Link。

原因

  • 单向数据流更清晰,易于理解和维护
  • 避免复杂的双向依赖关系
  • 便于调试和测试
// ✅ 推荐:单向数据流
@Entry
@Component
struct Parent {
  @State data: string = 'value'

  handleChange(newValue: string) {
    this.data = newValue
  }

  build() {
    Child({
      value: this.data,
      onChange: (val) => this.handleChange(val)
    })
  }
}

// ⚠️ 谨慎使用:双向绑定
@Entry
@Component
struct Parent {
  @State data: string = 'value'

  build() {
    Child({ value: $data })
  }
}

9.2 组件封装建议

展示型组件:使用 @Prop

@Component
export struct ProductCard {
  @Prop name: string
  @Prop price: number
  @Prop image: string

  build() {
    // 只展示,不修改
  }
}

容器型组件:使用 @State

@Entry
@Component
struct ProductList {
  @State products: Product[] = []

  build() {
    List() {
      ForEach(this.products, (product: Product) => {
        ProductCard({
          name: product.name,
          price: product.price,
          image: product.image
        })
      })
    }
  }
}

表单组件:使用 @Link

@Component
export struct CustomInput {
  @Link value: string
  private label: string = ''

  build() {
    Column() {
      Text(this.label)
      TextInput({ text: this.value })
        .onChange((text) => {
          this.value = text
        })
    }
  }
}

9.3 命名规范

// @State:描述性名称
@State isLoading: boolean = false
@State userList: User[] = []
@State currentIndex: number = 0

// @Prop:与数据含义一致
@Prop productName: string
@Prop userName: string
@Prop itemCount: number

// @Link:强调共享性
@Link selectedItems: string[]
@Link formData: FormData
@Link sharedCount: number

✅ 检查清单

在使用装饰器前,问自己这些问题:

@State 检查清单

  • 这个状态只在当前组件使用吗?
  • 不需要从父组件接收吗?
  • 已经赋初值了吗?

@Prop 检查清单

  • 这个数据从父组件接收吗?
  • 子组件只需要展示,不需要修改吗?
  • 父组件变化时,子组件需要更新吗?
  • 传递时直接传值了吗(没有用 $)?

@Link 检查清单

  • 这个数据需要父子同步吗?
  • 子组件需要修改数据吗?
  • 修改需要同步到父组件吗?
  • 传递时使用 $ 符号了吗?

🎬 总结

一句话总结

@State 管理组件内部状态,@Prop 实现父向子单向传递,@Link 实现父子双向绑定。三者的选择取决于数据的来源和使用方式。

核心原则

  1. 能用 @State 就用 @State(组件私有)
  2. 优先使用 @Prop(单向数据流)
  3. 必要时才用 @Link(双向绑定)

记住:正确使用这三个装饰器,能让你的代码更清晰、更易维护!


祝你开发顺利! 🚀

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐