《鸿蒙开发必备:V1 装饰器(@Observed&@ObjectLink 等)用法与原理》
总结了ArkUI框架中的状态管理机制:1. @Observed和@ObjectLink用于嵌套对象的响应式更新,@Observed修饰类,@ObjectLink修饰组件变量建立双向绑定;2. @Link实现父子组件双向同步,但不能本地初始化;3. @Provide和@Consume支持跨组件双向数据同步;4. @Watch监听状态变化,需配合其他装饰器使用;5. @BuilderParam实现组件
1. @Observed&@ObjectLink
作用: 用于嵌套对象或类对象属性的响应式更新(修改多层数据嵌套的值、可以引起UI更新)
v1版本中修改多层数据的嵌套,无法引起UI更新
要么直接修改整个对象的第一层数据,要么用@Observed&@ObjectLink解决数据更新问题
@Observed: 用于装饰类,表示这个类的实例可以被观察,当类属性变化时会触发UI更新
@ObjectLink: 用于装饰组件中的变量,表示这个变量与父组件中的@Observed对象建立双向绑定
注意事项
- @Observed必须修饰类,不能直接修饰基本类型或数组
- @ObjectLink只能用于被@Observed修饰的类的实例
- @ObjectLink不能给初始值—>不然报错,解决:让父组件传递数据给初始值
案例的改写:解决数据多层嵌套修改值时,无法引起UI更新
之前是修改第一层数据的整条对象,性能消耗过大,且先销毁在创建导致头像闪烁
核心步骤:
- interface 改为 类 class 并使用 @Observed 修饰组件中的变量
- 通过 new 的方式完成数据的创建
- 状态修饰符改为 @ObjectLink(双向数据同步)在子组件修改数据:直接修改
// 数据接口
interface IReplyItem {
id: number
avatar: ResourceStr
author: string
content: string
time: string
area: string
likeNum: number
likeFlag: boolean
}
@Observed
class ReplyItem{
id: number
avatar: ResourceStr
author: string
content: string
time: string
area: string
likeNum: number
likeFlag: boolean
constructor(value:IReplyItem) {
this.id = value.id
this.avatar =value. avatar
this.author = value.author
this.content = value.content
this.time = value.time
this.area =value. area
this.likeNum = value.likeNum
this.likeFlag = value.likeFlag
}
}
class ReplyData {
static getCommentList(): ReplyItem[] {
return [
new ReplyItem( {
id: 1,
avatar: 'https://picx.zhimg.com/027729d02bdf060e24973c3726fea9da_l.jpg?source=06d4cd63',
author: '偏执狂-妄想家',
content: '更何况还分到一个摩洛哥[惊喜]',
time: '11-30',
area: '海南',
likeNum: 34,
likeFlag: false
}),
new ReplyItem({
id: 2,
avatar: 'https://pic1.zhimg.com/v2-5a3f5190369ae59c12bee33abfe0c5cc_xl.jpg?source=32738c0c',
author: 'William',
content: '当年希腊可是把1:0发挥到极致了',
time: '11-29',
area: '北京',
likeNum: 58,
likeFlag: true
}),
new ReplyItem({
id: 3,
avatar: 'https://picx.zhimg.com/v2-e6f4605c16e4378572a96dad7eaaf2b0_l.jpg?source=06d4cd63',
author: 'Andy Garcia',
content: '欧洲杯其实16队球队打正赛已经差不多,24队打正赛意味着正赛阶段在小组赛一样有弱队。',
time: '11-28',
area: '上海',
likeNum: 10,
likeFlag: false
}),
new ReplyItem({
id: 4,
avatar: 'https://picx.zhimg.com/v2-53e7cf84228e26f419d924c2bf8d5d70_l.jpg?source=06d4cd63',
author: '正宗好鱼头',
content: '确实眼红啊,亚洲就没这种球队,让中国队刷',
time: '11-27',
area: '香港',
likeNum: 139,
likeFlag: true
}),
new ReplyItem({
id: 5,
avatar: 'https://pic1.zhimg.com/v2-eeddfaae049df2a407ff37540894c8ce_l.jpg?source=06d4cd63',
author: '柱子哥',
content: '我是支持扩大的,亚洲杯欧洲杯扩到32队,世界杯扩到64队才是好的,世界上有超过200支队伍,欧洲区55支队伍,亚洲区47支队伍,即使如此也就六成出现率',
time: '11-27',
area: '旧金山',
likeNum: 29,
likeFlag: false
}) ,
new ReplyItem({
id: 6,
avatar: 'https://picx.zhimg.com/v2-fab3da929232ae911e92bf8137d11f3a_l.jpg?source=06d4cd63',
author: '飞轩逸',
content: '禁止欧洲杯扩军之前,应该先禁止世界杯扩军,或者至少把亚洲名额一半给欧洲。',
time: '11-26',
area: '里约',
likeNum: 100,
likeFlag: false
})
]
}
static getRootComment(): ReplyItem {
return new ReplyItem({
id: 1,
avatar: $r("app.media.background"),
author: '周杰伦',
content: '意大利拌面应该使用42号钢筋混凝土再加上量子力学缠绕最后通过不畏浮云遮望眼',
time: '11-30',
area: '海南',
likeNum: 98,
likeFlag: true
})
}
}
@Component
// 评论组件
struct replyComment {
// 评论数据由外部传入,会更改点赞个数,所以通过@Prop 定义
// @Prop reply: Partial<ReplyItem> = {}
@ObjectLink reply: ReplyItem//不用@Prop单项接受数据了,改成@ObjectLink(可双向数据同步)
// 切换点赞 ,逻辑由外部传入,这里定义为空的箭头函数即可
changeLike = () => {}
build() {
Row() {
Image(this.reply.avatar)// 头像
.width(32)
.height(32)
.borderRadius(16)
Column({ space: 10 }) {
Text(this.reply.author)// 作者
.fontWeight(600)
Text(this.reply.content)// 内容
.lineHeight(20)
.fontSize(14)
.fontColor("#565656")
Row() {
Text(`${this.reply.time} . IP属地 ${this.reply.area}`)// 时间+区域
.fontColor("#c3c4c5")
.fontSize(12)
Row() {
Image($r("sys.media.AI_search"))
.width(14)
.aspectRatio(1)// 是否点赞
.fillColor(this.reply.likeFlag ? Color.Red : "#c3c4c5") // "#c3c4c5" 或 red
Text(this.reply.likeNum?.toString())// 点赞数
.fontSize(12)
.margin({left: 5 })
}// 点击 心和数组均会触发这个效果,所以点击事件绑定给 Row
.onClick(() => {this.changeLike() })
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.padding({
left: 15,
right: 5
})
}
.justifyContent(FlexAlign.Start)
.alignItems(VerticalAlign.Top)
.width('100%')
.padding(15)
}
}
// 底部发布评论组件
@Component
struct addReply {
@State iptString: string = ''
// 点击发布评论时执行的方法
// 参数 1:需要传递给父组件的 输入值
clickPublish = (reply: string) => {
}
build() {
Row() {
TextInput({ placeholder: '回复', text: $$this.iptString })
.layoutWeight(1)
.backgroundColor("#f4f5f6")
.height(40)
Text('发布')
.fontColor("#6ecff0")
.fontSize(14)
.margin({
left: 10
})
.onClick(() => {
this.clickPublish(this.iptString)
})
}
.padding(10)
.backgroundColor(Color.White)
.border({
width: { top: 1 },
color: "#f4f5f6"
})
}
}
@Entry
@Component
struct Page02_zhihu {
// 评论列表 数组
@State commentList: ReplyItem[] = ReplyData.getCommentList()
// 顶部评论 对象
@State rootComment: ReplyItem = ReplyData.getRootComment()
build() {
Column() {
// 上半部分
Column() {
Column() {
// 顶部区域
Row() {
Row() {
Image($r('sys.media.ohos_ic_public_arrow_left'))
.width(20)
.height(20)
}
.borderRadius(20)
.backgroundColor('#f6f6f6')
.justifyContent(FlexAlign.Center)
.width(30)
.aspectRatio(1)
.margin({
left: 15
})
Text("评论回复")
.layoutWeight(1)
.textAlign(TextAlign.Center)
.padding({
right: 35
})
}
.width('100%')
.height(50)
.border({
width: {
bottom: 1
},
color: '#f6f6f6',
})
// 顶部评论
replyComment({
reply: this.rootComment, changeLike: () => {
// 修改状态
this.rootComment.likeFlag = !this.rootComment.likeFlag
// 根据点赞的状态 修改个数
if (this.rootComment.likeFlag == true) {
this.rootComment.likeNum++
} else {
this.rootComment.likeNum--
}
}
})
// 分割线
Divider()
.strokeWidth(6)
.color("#f4f5f6")
// 回复数
Text() {
Span('回复')
Span(`${this.commentList.length}`)
}
.padding(15)
.fontWeight(700)
.alignSelf(ItemAlign.Start)
// 回复评论列表
List() {
ForEach(this.commentList, (item: ReplyItem, index: number) => {
ListItem() {
replyComment({
reply: item, changeLike: () => {
// 修改状态
item.likeFlag = !item.likeFlag
// 修改个数
if (item.likeFlag == true) {
item.likeNum++
} else {
item.likeNum--
}
// 上述代码执行完毕之后,数据已经改变了,但是页面不更新,因为是深层属性
// 通过 splice 方法触发更新(页面会闪,明天讲解决方案)
// this.commentList.splice(index, 1, item)
}
})
}
})
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.divider({ strokeWidth: 1, startMargin: 40, endMargin: 10 })
}
.width('100%')
.backgroundColor(Color.White)
}
.layoutWeight(1)
// 底部的发布评论区域
addReply({
clickPublish: (reply: string) => {
this.commentList.unshift(new ReplyItem({//改成new,因为数据类型改成了class
id: Date.now(),
avatar: $r('app.media.background'),
author: '小黑',
content: reply,
time: `${new Date().getMonth() + 1}-${new Date().getDate()}`,
area: '北京',
likeNum: 0,
likeFlag: false
}))
}
})
}
.height('100%')
}
}

2. @Link双向同步
作用: 与父组件建立双向数据同步
注意事项:
- @Link 不能本地初始化, 必须从父组件进行初始化
- @Link 不能接收ForEach中对象数组数据
使用步骤:
- 将父组件的状态属性传递给子组件
- 子组件通过@Link修饰即可
- 简单数据和复杂数据的更新都能检测到,但复杂数据的值依然只能检测到第一层的变化,如果多层嵌套修改,用@Observed&@ObjectLink
interface LinKDemo {
name?: string
age: number
}
@Entry // 父组件
@Component
struct KnowledgePageF {
//必须加@state或者其他装饰器
// (因为子组件用@link接收的数据,有可能会双向数据更新)
@State count: number = 0
@State obj: LinKDemo = { age: 1 }
build() {
Column() {
Text('父组件-简单数据: ' + this.count)
.fontSize(30)
Text('父组件-复杂数据: ' + this.obj.age)
.fontSize(30)
.margin({bottom:20})
Button('修改数据')
.onClick(() => {
this.count++
this.obj.age++
})
SonsComponent({
sonDemo: this.count,
sonObj: this.obj
})
}
.padding(10)
.height('100%')
.backgroundColor('#ccc')
.width('100%')
.alignItems(HorizontalAlign.Center)
.padding({ top: 100 })
}
}
@Component // 子组件
struct SonsComponent {
//Link不能初始化 (让父组件去初始化)
@Link sonDemo: number
@Link sonObj: LinKDemo
build() {
Column({ space: 20 }) {
Text('子组件-简单数据: ' + this.sonDemo)
.fontSize(20)
Text('子组件-复杂数据: ' + this.sonObj.age)
.fontSize(20)
Button('修改数据')
.onClick(() => {
this.sonDemo++
this.sonObj.age++
})
}
.backgroundColor(Color.Pink)
.alignItems(HorizontalAlign.Center)
.width('80%')
.margin({ top: 100 })
.padding(10)
.borderRadius(10)
}
}
@Entry//父组件
@Component
struct Day09_LinkForEach {
build() {
Column() {
ForEach(Array.from({ length: 5 }), (item: number) => {
SonLinkForEach({ item: item })
})
}
.height('100%').width('100%')
}
}
@Component//子组件
struct SonLinkForEach {
//1.@Link双向数据同步
//注意点: 1.不能自己初始化值(让父组件初始化)、2.不能通过ForeEach循环传递数据
// @Link item:number
@Prop item: number//使用@prop接收-不报错
build() {
Column() {
Text('111')
}
}
}


3. @Provide、@Consume跨组件传值
作用: 用于跨层级组件(祖先与后代)之间的数据同步
注意事项:
- @Consume 不能本地初始化, 从父组件进行初始化
- 通过相同的变量名绑定,也可以修改变量名——>@Consume('变量名') 更改的名字: 类型
// 写法 1:通过相同的变量名绑定
@Provide a: number = 0;
@Consume a: number;
// 写法 2通过相同的变量别名绑定
@Provide b: number = 0;
@Consume('b') c: number;
使用步骤:
- 将父组件的状态属性使用@Provide修饰
- 子组件通过@Consume修饰
interface objS{
age:number
}
@Entry // 顶级组件
@Component
struct RootComponent {
@Provide count:number=0
@Provide obj: objS={age:10}
build() {
Column() {
Text('顶级组件 '+this.count+'-'+this.obj.age)
.fontSize(30)
.fontWeight(900)
ParentComponent()//二级组件
}
.padding(10)
.height('100%')
.backgroundColor('#ccc')
.width('100%')
.alignItems(HorizontalAlign.Center)
.padding({ top: 100 })
}
}
@Component
// 二级组件
struct ParentComponent {
@Consume count:number
@Consume obj:objS
build() {
Column({ space: 20 }) {
Text('我是二级组件'+this.count+'-'+this.obj.age)
.fontSize(22)
.fontWeight(900)
SonComponentDemo()//三级组件
}
.backgroundColor('#a6c398')
.alignItems(HorizontalAlign.Center)
.width('90%')
.margin({ top: 50 })
.padding(10)
.borderRadius(10)
}
}
@Component
// 内层组件
struct SonComponentDemo {
//Consume不能初始化
@Consume count:number
//复杂数据类型也可以双向传递(多层嵌套不行,只能更新第一层)
@Consume obj:objS
build() {
Column({ space: 20 }) {
Text('我是内层组件'+this.count+'-'+this.obj.age)
.fontSize(20)
.fontWeight(900)
.onClick(()=>{
this.count++
this.obj.age++
})
}
.backgroundColor('#bf94e4')
.alignItems(HorizontalAlign.Center)
.width('90%')
.margin({ top: 50 })
.padding(10)
.borderRadius(10)
}
}

跨组件传值注意事项

4. @Watch-状态监听器
作用: 监听某个状态变量的值是否改变
注意事项:
- @Watch无法单独使用,必须配合状态装饰器,比如@State、@Prop、@Link(@watch写在装饰器后面,变量的前面)
- 当状态变量值发生更改会调用此方法
import { promptAction } from '@kit.ArkUI'
@Entry // 父组件
@Component
struct KnowledgePage {
// @Watch
/* 1.不能单独使用需要和其他装饰器配合2.当状态变量值发生更改会调用此方法*/
@State
@Watch('changCount')
countWatch: number = 0
// 方法 当状态变量值发生更改会调用此方法
changCount(){
promptAction.openToast({
message: '数据变化了',
bottom: ''
})
}
build() {
Column() {
Text('父组件')
.fontSize(30)
Text(this.countWatch+'')
Button('修改数据')
.onClick(() => {
this.countWatch += 2
})
SonComponent({ countWatch: this.countWatch })
}
.padding(10)
.height('100%')
.backgroundColor('#ccc')
.width('100%')
.alignItems(HorizontalAlign.Center)
.padding({ top: 100 })
}
}
@Component
// 子组件
struct SonComponent {
@Link countWatch: number
// 编写 UI
build() {
Column({ space: 20 }) {
Text('我是子组件')
.fontSize(20)
Text(this.countWatch+'')
Column() {
Button('修改countWatch')
.onClick(() => {
this.countWatch++
})
}
}
.backgroundColor('#a6c398')
.alignItems(HorizontalAlign.Center)
.width('80%')
.margin({ top: 30 })
.padding(10)
.borderRadius(10)
}
}

5. @BuilderParam自定义结构内容
@BuilderParam——类比前端插槽slot
在一个组件中定义另一个组件,叫做尾随闭包
使用BuilderParam的步骤
- 需要出现父子组件的关系
- @BuilderParam应出现在子组件中
- @BuilderParam的类型是一个箭头函数
- BuilderParam的参数可给初始值 也可以不给初始值
如果给了初始值, 就是没有内容的默认内容,@BuilderParam 初始值必须是Builder修饰的
- 父组件传入的时候,传入的是用@Builder书写的结构
避免this指向问题,在父组件向子组件传递@Builder结构时,用箭头函数包裹@Builder结构

5.1. BuilderParam骨架屏

6. 状态通讯小结:
- @State: 修饰之后,引起UI更新
- @Prop: 父子组件单向通信(@prop可以初始化-也可以不初始化,不初始化时默认是underfind)
- @Observed&@ObjectLink: 用于嵌套对象或类对象属性的响应式更新
- @Link: 父子双向通信(@link不能初始化)
- @Provide&@Consume: 跨组件双向通信(@Consume不能初始化)
- @Watch: 监视数据,数据改变 -> 执行方法
更多推荐




所有评论(0)