【HarmonyOS】深入理解@Observed装饰器和@ObjectLink装饰器:嵌套类对象属性变化
HarmonyOS NEXT / HarmonyOS 5 ArkUI 状态管理系列
本文适用版本:API12 ~ API19(HarmonyOS NEXT)
前言
相信很多刚接触 HarmonyOS ArkUI 的开发者,都遇到过这样一个问题:
例如我们有这样一个对象:
class User {
name: string = "Tom"
age: number = 18
}
页面这样写:
@State
user: User = new User()
然后修改:
this.user.name = "Jack"
结果……
页面没有刷新!
很多人第一反应:
不是@State吗?为什么不刷新?
随后开始尝试:
- this.user = new User()
- this.user = Object.assign(...)
- this.user = JSON.parse(JSON.stringify(...))
- this.user = {...this.user}
终于刷新了。
于是很多人认为:
HarmonyOS只能修改整个对象。
其实……
并不是。
真正原因是:
ArkUI 默认只能监听对象引用变化,而不会监听对象内部属性变化。
这就是今天要讲的:
@Observed + @ObjectLink
它们就是 HarmonyOS 官方提供的:
对象属性级响应式刷新机制。
很多企业项目:
- IM聊天
- 通讯录
- 购物车
- 用户信息
- 商品详情
- 评论列表
几乎都会使用它们。
如果不会:
后期项目几乎一定踩坑。
本文将彻底讲透它们的底层原理。
一、先理解 ArkUI 为什么不能监听对象内部变化
先来看一个最经典例子。
示例一
class User {
name: string = "Tom"
}
@Entry
@Component
struct Index {
@State
user: User = new User()
build() {
Column() {
Text(this.user.name)
Button("修改姓名")
.onClick(()=>{
this.user.name = "Jack"
})
}
}
}
很多新人觉得:
name变了。
Text应该刷新。
但是:
实际运行:
Tom
点击按钮以后:
Tom
没有变化。
为什么?
真正原因
ArkUI监听的是:
user
不是:
user.name
也就是说:
@State
↓
监听的是对象引用
例如:
user
│
├──name
├──age
└──sex
ArkUI只关心:
user地址有没有变
例如:
0x0012456
如果:
this.user.name="Jack"
那么:
内存其实变成:
0x0012456
name
Tom
↓
Jack
对象地址:
没有变
所以:
ArkUI认为:
没有变化
因此:
不会重新build。
二、真正触发刷新的是什么?
很多人一直误认为:
修改变量。
页面刷新。
实际上:
真正刷新的是:
对象引用变化。
例如:
this.user = new User()
或者:
this.user = {
name:"Jack"
}
引用发生变化:
旧对象
0x001111
↓
新对象
0x009999
ArkUI检测到:
地址变了
于是:
重新build。
流程如下:
赋值
↓
引用变化
↓
State通知
↓
MarkDirty
↓
重新build
↓
Diff
↓
Render
这就是整个刷新流程。
三、为什么 Vue 可以监听?
很多人学习 HarmonyOS 前都学过:
Vue3。
Vue里面:
user.name="Jack"
页面刷新。
为什么?
因为:
Vue底层用了:
Proxy
例如:
let user = new Proxy(obj,{
set(){
update()
}
})
每修改一个属性:
都会进入:
set()
然后:
通知页面。
例如:
user.name="Jack"
↓
Proxy.set
↓
update()
↓
重新render
所以:
Vue天然支持:
对象属性监听。
而 ArkUI 默认并不是这样。
默认情况下:
@State
不会给对象建立:
Proxy
所以:
属性变化:
没人知道。
四、HarmonyOS 为什么没有默认监听所有对象?
很多开发者问:
既然 Vue 可以。
HarmonyOS 为什么不全部监听?
答案:
性能。
假设:
一个购物车:
class Cart{
goodsList:Goods[]
}
里面:
10000个商品
每个商品:
20多个字段
例如:
商品
价格
库存
图片
规格
销量
活动
品牌
...
如果:
全部Proxy。
那么:
初始化时:
需要:
10000
×
20
=
200000
个属性
全部建立监听。
这会导致:
初始化成本巨大。
尤其:
企业项目:
订单
↓
商品
↓
SKU
↓
活动
↓
库存
↓
优惠券
可能:
嵌套五六层。
如果:
全部递归Proxy:
第一次进入页面:
就会卡顿。
因此:
HarmonyOS采用:
按需监听。
需要监听:
你告诉它:
@Observed
不用监听:
普通Class。
这样:
性能更高。
五、@Observed 到底是什么?
官方解释:
使一个Class成为可观察对象。
一句话:
就是:
给整个对象增加属性监听能力。
例如:
普通类:
class User{
}
不能监听。
改成:
@Observed
class User{
}
立即变成:
Observable Object
也就是说:
以后:
name
变化:
age
变化:
address
变化:
都可以通知UI。
例如:
@Observed
class User{
name:string="Tom"
age:number=18
}
这时候:
name
不是普通变量。
而是:
Observable Property
内部会拥有:
getter
setter
或者:
Proxy能力。
于是:
this.user.name="Jack"
会触发:
notifyPropertyChanged()
然后:
通知:
所有引用它的组件。
六、仅有 @Observed 能刷新页面吗?
答案:
不能。
这是整个 HarmonyOS 状态管理中最容易踩的坑。
很多人写成:
@Observed
class User {
name:string="Tom"
}
然后:
@State
user:User=new User()
点击:
this.user.name="Jack"
结果:
还是没刷新。
为什么?
因为:
虽然:
User
已经:
可以发送通知。
但是:
没有任何组件接收这个通知。
就像:
广播站
一直广播
但是:
没人收听。
广播:
没有意义。
所以:
还需要:
@ObjectLink
它负责:
订阅 @Observed 对象发送的属性变化通知,并与父组件保持对象引用同步。
也就是说:
@Observed
负责发送通知
↓
@ObjectLink
负责接收通知
↓
组件刷新
二者缺一不可。
本篇小结
这一部分,我们完成了几个最核心的知识点:
- 为什么
@State默认不能监听对象内部属性变化。 - ArkUI 默认监听的是对象引用,而不是对象成员。
- 为什么重新赋值可以刷新,而修改成员不会刷新。
- HarmonyOS 为什么没有像 Vue 一样默认代理所有对象(性能考虑)。
@Observed的职责:让类具备属性变化通知能力。- 为什么只有
@Observed还不够,还需要@ObjectLink建立组件与对象之间的响应式连接。
七、为什么 @Observed 和 @ObjectLink 必须一起使用?
很多开发者都会问:
既然 @Observed 已经可以监听对象属性变化了,为什么还需要 @ObjectLink?
我们先来看官方推荐的使用方式。
官方标准写法
@Observed
class User {
name: string = 'Tom'
age: number = 18
}
父组件:
@Entry
@Component
struct ParentPage {
@State
user: User = new User()
build() {
Column() {
Child({
user: this.user
})
Button("修改姓名")
.onClick(() => {
this.user.name = "Jack"
})
}
}
}
子组件:
@Component
struct Child {
@ObjectLink
user: User
build() {
Text(this.user.name)
}
}
运行以后:
第一次进入页面:
Tom
点击按钮:
Jack
整个过程:
父组件
↓
修改对象属性
↓
Observed通知
↓
ObjectLink收到通知
↓
Child刷新
↓
Text更新
整个刷新过程中:
没有重新创建User对象。
八、如果不用 @ObjectLink 会发生什么?
例如:
很多人这样写:
@Component
struct Child {
@Prop
user: User
build() {
Text(this.user.name)
}
}
或者:
@Component
struct Child {
@State
user: User
}
甚至:
@Component
struct Child {
user: User
}
都会出现一个现象:
父组件修改:
this.user.name = "Jack"
子组件:
没有刷新
为什么?
因为:
这些装饰器:
都没有建立:
对象监听关系
只有:
ObjectLink
↓
订阅Observed对象
所以:
官方明确规定:
Observed
+
ObjectLink
配套使用
九、@ObjectLink 到底是什么?
很多人误认为:
ObjectLink
=
Link
实际上:
完全不是一个东西。
它真正作用:
建立组件和Observed对象之间的数据观察关系。
可以理解成:
Observed
负责广播
↓
ObjectLink
负责订阅
例如:
User
↓
name变化
↓
广播通知
↓
ObjectLink收到
↓
组件刷新
如果没有ObjectLink:
广播没人接收。
可以理解成消息订阅模型
例如:
微信群。
发送消息:
张三:
大家晚上聚餐。
如果:
没人进群。
消息:
没有意义。
ObjectLink:
就是:
加入群聊
之后:
只要:
User对象:
发生变化。
ObjectLink:
立即收到。
然后:
通知UI刷新。
十、@ObjectLink 底层到底保存了什么?
实际上:
它保存的:
不是对象。
而是:
对象引用
+
监听关系
例如:
父组件:
0x00112233
User
子组件:
ObjectLink
↓
0x00112233
注意:
这里:
没有复制对象。
而是:
共同引用
同一个User
所以:
父组件:
user.name="Jack"
子组件:
立即就是:
Jack
因为:
它们:
操作的是:
同一块内存。
内存示意图:
Parent
↓
0x001000
↑
↓
Child
共同指向:
User
而不是:
Parent
↓
User①
Child
↓
User②
所以:
ObjectLink:
是真正意义上的:
双向引用
十一、ObjectLink 为什么叫 Link?
因为:
它建立的是:
Link
链接
绑定
关联
不是:
复制。
例如:
Parent
↓
User
↑
↓
Child
始终:
同一个对象。
所以:
Child:
修改:
this.user.name="Lucy"
Parent:
立即就是:
Lucy
无需:
任何:
callback。
无需:
emit。
无需:
EventHub。
无需:
重新赋值。
十二、ObjectLink 双向同步演示
父组件:
@Observed
class User{
name:string="Tom"
}
@Entry
@Component
struct Index{
@State
user:User=new User()
build(){
Column(){
Text("父:"+this.user.name)
Child({
user:this.user
})
}
}
}
子组件:
@Component
struct Child{
@ObjectLink
user:User
build(){
Column(){
Text("子:"+this.user.name)
Button("修改")
.onClick(()=>{
this.user.name="Lucy"
})
}
}
}
运行:
父:Tom
子:Tom
点击:
修改
结果:
父:Lucy
子:Lucy
整个过程中:
父组件:
没有:
setState
没有:
重新赋值
依旧:
同步。
这就是:
ObjectLink:
最大的价值。
十三、ObjectLink 的刷新流程
很多文章:
到这里:
就结束了。
实际上:
真正重要的是:
刷新流程。
例如:
点击按钮:
this.user.name="Lucy"
到底:
发生什么?
整个流程:
点击Button
↓
修改属性
↓
setter()
↓
Observed检测变化
↓
通知SubscriberManager
↓
找到ObjectLink
↓
ObjectLink通知Component
↓
Component Dirty
↓
ArkUI重新build
↓
Diff
↓
RenderNode更新
↓
GPU绘制
企业项目:
理解这一流程:
非常重要。
因为:
后面:
性能优化:
全部基于这里。
十四、Subscriber(订阅者)到底是什么?
ArkUI内部:
其实:
维护了一张:
订阅关系表
类似:
User①
↓
Child①
↓
Child②
↓
Child③
例如:
三个页面:
同时:
引用:
同一个User。
Profile
↓
User
↑
Friend
↑
Chat
User:
修改:
name
系统:
立即:
找到:
三个Subscriber
然后:
全部刷新。
所以:
一个对象:
可以:
通知:
多个组件。
这也是:
企业IM、
企业通讯录、
企业CRM:
大量使用ObjectLink的原因。
十五、多个 ObjectLink 可以同时绑定一个对象吗?
答案:
可以,而且非常常见。
例如:
主页
↓
User
↑
消息中心
↑
我的页面
↑
设置页面
四个页面:
全部:
ObjectLink。
修改:
头像
立即:
四个地方:
一起刷新。
无需:
EventHub
Emitter
通知广播
手动刷新
这就是:
响应式数据模型。
十六、ObjectLink 与 @Link 的本质区别
很多初学者最容易混淆这两个装饰器,它们虽然名字都带 Link,但解决的问题完全不同。
| 对比项 | @Link | @ObjectLink |
|---|---|---|
| 绑定普通变量 | ✅ | ❌ |
绑定 @Observed 对象 |
❌ | ✅ |
| 支持对象属性监听 | ❌ | ✅ |
| 双向修改对象成员 | ❌ | ✅ |
| 引用同一个对象 | 部分场景 | ✅ |
| 推荐用于嵌套对象 | ❌ | ✅ |
举个例子:
@State
count: number = 1
子组件:
@Link
count: number
这是 @Link 最典型的使用场景。
而:
@Observed
class User {
name: string = ""
}
则应该使用:
@ObjectLink
user: User
如果误用 @Link 来接收 User,对象成员变化不会触发子组件响应式刷新。
本篇小结
这一部分重点讲解了:
- 为什么
@Observed和@ObjectLink必须配套使用。 @ObjectLink本质上是 观察者(Subscriber),负责订阅@Observed对象。@ObjectLink保存的是对象引用而不是对象副本,因此父子组件共享同一个实例。@ObjectLink支持真正的双向同步,无需事件总线、回调或重新赋值。- ArkUI 内部通过 订阅关系(Subscriber) 完成对象属性变化后的组件刷新。
十七、为什么加了 @Observed 还是不刷新?
这是很多开发者遇到最多的问题。
代码:
@Observed
class User {
name:string = "Tom"
address:Address = new Address()
}
@Observed
class Address {
city:string = "Tokyo"
}
页面:
@State
user:User = new User()
修改:
this.user.address.city = "Osaka"
结果:
页面:
不刷新。
很多人疑惑:
User 有 @Observed,Address 也有 @Observed,为什么还是不行?
原因:
对象监听具有层级关系。
十八、理解嵌套对象监听模型
我们先看对象结构:
User
|
|
+---- name
|
|
+---- address
|
|
+---- city
实际上:
这里存在三个监听关系:
第一层:
User
第二层:
Address
第三层:
city
如果想监听:
user.address.city
必须:
每一层:
都是可观察对象。
也就是:
@Observed
class User {
}
@Observed
class Address {
}
缺少任何一层:
都会失效。
十九、错误示例:只给外层加 @Observed
例如:
@Observed
class User {
address:Address = new Address()
}
class Address {
city:string="Tokyo"
}
修改:
this.user.address.city="Osaka"
不会刷新。
原因:
User知道:
address变化
但是不知道:
address.city变化
因为:
Address不是Observed对象。
二十、正确写法:所有需要观察的层级都加 @Observed
正确:
@Observed
class Address {
city:string="Tokyo"
}
@Observed
class User {
name:string="Tom"
address:Address = new Address()
}
然后:
组件:
@Component
struct Child {
@ObjectLink
user:User
build(){
Text(this.user.address.city)
}
}
修改:
this.user.address.city="Osaka"
刷新流程:
city变化
↓
Address setter
↓
Address通知
↓
User关联对象收到
↓
ObjectLink收到
↓
Child刷新
↓
Text更新
二十一、嵌套对象最大的坑:对象初始化顺序
例如:
错误:
@Observed
class User {
address?:Address
}
然后:
this.user.address =
{
city:"Tokyo"
}
问题:
这个对象:
不是Observed实例。
它只是普通Object。
也就是说:
你虽然定义:
@Observed
class Address
但是实际:
赋值的是:
{
city:"Tokyo"
}
不是:
new Address()
正确:
this.user.address =
new Address()
或者:
class Address {
city:string
constructor(city:string){
this.city=city
}
}
然后:
new Address("Tokyo")
二十二、为什么对象字面量经常导致刷新失败?
很多JavaScript开发习惯:
喜欢:
const user={
name:"Tom",
age:18
}
但是:
ArkUI状态管理:
更加推荐:
class User{
name:string="Tom"
age:number=18
}
原因:
class可以:
被:
@Observed
装饰。
而:
object:
没有生命周期。
例如:
普通对象:
{
name:"Tom"
}
内部:
没有:
observer
subscriber
notify
所以:
无法通知UI。
二十三、@Observed 不会递归监听普通对象
这是一个非常重要的知识点。
很多人认为:
@Observed
class User{
data:any
}
然后:
data里面所有东西:
自动监听。
错误。
例如:
@Observed
class User{
info={
name:"Tom",
age:18
}
}
修改:
this.user.info.name="Jack"
不会刷新。
原因:
info:
只是普通Object。
结构:
User
Observed
|
+---- info
普通Object
|
+----name
+----age
监听链:
断了。
二十四、正确处理复杂对象
方案一:
全部Class化。
例如:
@Observed
class UserInfo{
name:string="Tom"
age:number=18
}
@Observed
class User{
info:UserInfo=new UserInfo()
}
这样:
完整响应链:
User
↓
UserInfo
↓
name
全部可观察。
方案二:
使用状态管理提供的复杂对象能力。
HarmonyOS 新版本:
针对复杂对象:
提供了:
makeV1Observed
等能力。
适用于:
已有业务Model。
例如:
服务器返回:
{
"user":{
"name":"Tom",
"address":{
"city":"Tokyo"
}
}
}
不方便全部改成class。
可以:
转换成可观察对象。
二十五、List中使用 @Observed + @ObjectLink
企业项目最常见:
列表。
例如:
通讯录:
[
{
name:"Tom"
},
{
name:"Jack"
}
]
错误方式:
@State
users:Array<User>
然后:
修改:
users[0].name="Lucy"
可能:
无法刷新。
原因:
数组监听:
和对象监听:
是两个层次。
数组:
监听:
push
pop
splice
替换
但是:
不一定监听:
item.name
二十六、正确列表模型
定义:
@Observed
class Contact{
name:string=""
phone:string=""
}
页面:
@State
contacts:Array<Contact>=[]
ForEach:
ForEach(
this.contacts,
(item:Contact)=>{
ContactItem({
contact:item
})
}
)
子组件:
@Component
struct ContactItem{
@ObjectLink
contact:Contact
build(){
Text(this.contact.name)
}
}
修改:
this.contacts[0].name="Lucy"
结果:
对应item刷新。
而不是:
整个List刷新。
二十七、为什么 ObjectLink 对 List 性能更好?
假设:
通讯录:
10000条。
如果:
修改第一条:
普通方式:
整个List刷新
10000个Item重新build
性能:
差。
ObjectLink:
流程:
Contact对象变化
↓
找到对应ObjectLink
↓
刷新第一个Item
结果:
1个Item刷新
这就是:
大型应用:
必须使用:
对象观察。
二十八、对象数组刷新最佳实践
推荐:
@Observed
class Product {
id:number
name:string
price:number
}
数据:
@State
products:Product[]=[]
Item:
@Component
struct ProductItem {
@ObjectLink
product:Product
build(){
Row(){
Text(this.product.name)
Text(
this.product.price.toString()
)
}
}
}
修改:
product.price=99
只刷新:
当前商品。
二十九、常见错误总结
错误1
只加:
@Observed
没有:
@ObjectLink
结果:
对象变化:
没人接收。
错误2
嵌套对象没有全部Observed。
错误:
@Observed
class User{
address:{
city:string
}
}
错误3
使用普通Object。
错误:
this.user={
name:"Tom"
}
正确:
new User()
错误4
List直接修改item属性。
错误:
list[0].name="Jack"
没有Observed。
错误5
复制对象。
例如:
childUser={
...this.user
}
这样:
响应链断开。
三十、本篇总结
本篇核心:
1. @Observed不是万能监听
它只监听:
Observable对象。
2. 嵌套对象必须完整观察链
例如:
User
↓
Address
↓
City
每一层:
都需要支持观察。
3. 普通Object不会自动监听
推荐:
Class模型。
4. List场景必须结合ObjectLink
实现:
精准刷新。
5. 企业项目推荐数据结构
Model(Class)
↓
@Observed
↓
Page(@State)
↓
Component(@ObjectLink)
↓
精准刷新
三十一、企业项目为什么大量使用 @Observed + @ObjectLink?
在简单Demo中:
Text(this.user.name)
看不出优势。
但是企业项目:
数据结构通常是:
用户
├── 基础信息
├── 头像
├── VIP状态
├── 地址
├── 订单列表
├── 消息列表
└── 权限列表
一个页面:
可能同时展示:
头像
昵称
等级
订单数量
优惠券数量
消息数量
如果使用:
@State
user:User
修改:
user.avatar
很容易导致:
整个页面刷新。
而:
@Observed
+
@ObjectLink
可以做到:
头像变化
↓
Avatar组件刷新
昵称不变
↓
不刷新
订单不变
↓
不刷新
这就是:
细粒度响应式更新。
三十二、案例一:仿微信聊天列表
先设计数据模型。
聊天消息:
@Observed
class Message {
id:number = 0
content:string = ""
time:string = ""
unread:number = 0
}
聊天对象:
@Observed
class Conversation {
userName:string=""
avatar:string=""
lastMessage:Message =
new Message()
}
这里注意:
两个类:
都有:
@Observed
原因:
存在嵌套关系:
Conversation
|
|
Message
|
|
unread
如果:
修改:
conversation.lastMessage.unread++
需要:
完整观察链。
三十三、页面数据
首页:
@Entry
@Component
struct ChatPage {
@State
conversations:Array<Conversation> = []
build(){
Column(){
ForEach(
this.conversations,
(item:Conversation)=>{
ChatItem({
conversation:item
})
}
)
}
}
}
这里:
重点:
不是:
传递数据。
而是:
传递对象引用。
三十四、聊天Item组件
@Component
struct ChatItem {
@ObjectLink
conversation:Conversation
build(){
Row(){
Text(
this.conversation.userName
)
Text(
this.conversation.lastMessage.content
)
if(
this.conversation.lastMessage.unread>0
){
Text(
this.conversation.lastMessage.unread.toString()
)
}
}
}
}
这里:
ChatItem
不会复制:
Conversation。
而是:
连接:
页面Conversation
↓
ObjectLink
↓
ChatItem
三十五、收到新消息如何刷新?
假设:
服务器收到:
Tom:
晚上吃饭吗?
更新:
conversation.lastMessage.content =
"晚上吃饭吗"
conversation.lastMessage.unread++
发生:
Message.content变化
↓
Observed通知
↓
ObjectLink收到
↓
ChatItem刷新
↓
UI更新
注意:
没有:
this.conversations =
[
...
]
没有:
重新创建数组。
没有:
刷新整个List。
三十六、为什么大型聊天App必须这样设计?
假设:
微信:
500个聊天。
每个:
一个Item。
如果:
收到一条消息:
传统方式:
修改数据
↓
刷新List
↓
500个Item重新build
成本:
巨大。
ObjectLink:
Message变化
↓
找到对应Conversation
↓
找到对应ChatItem
↓
刷新一个
成本:
O(1)。
三十七、案例二:通讯录实现
通讯录:
结构:
Contact
|
+-- name
+-- phone
+-- avatar
+-- company
+-- department
定义:
@Observed
class Contact{
id:number=0
name:string=""
phone:string=""
avatar:string=""
}
列表:
@State
contacts:Array<Contact>=[]
Item:
@Component
struct ContactItem{
@ObjectLink
contact:Contact
build(){
Row(){
Image(
this.contact.avatar
)
Text(
this.contact.name
)
}
}
}
修改头像:
contact.avatar =
"https://xxx.png"
结果:
只有:
对应联系人刷新。
三十八、为什么不用@Prop?
很多人会问:
这里:
为什么不用:
@Prop
contact:Contact
?
原因:
@Prop:
是:
单向数据复制
流程:
Parent
↓
复制
↓
Child
而:
ObjectLink:
Parent
↓
同一个对象
↓
Child
例如:
@Prop:
父对象:
name=Tom
子对象:
name=Tom
两个值。
ObjectLink:
父
\
User对象
/
子
一个对象。
三十九、案例三:购物车
购物车是最经典案例。
数据:
Cart
|
+-- Product[]
|
+-- Product
|
+-- count
+-- price
商品模型:
@Observed
class Product{
id:number=0
name:string=""
price:number=0
count:number=1
}
购物车:
@Observed
class Cart{
products:Array<Product>=[]
}
页面:
@State
cart:Cart=new Cart()
商品Item:
@Component
struct ProductItem{
@ObjectLink
product:Product
build(){
Row(){
Text(
this.product.name
)
Text(
this.product.count.toString()
)
Button("+")
.onClick(()=>{
this.product.count++
})
}
}
}
点击增加数量:
count++
↓
Product setter
↓
通知
↓
ProductItem刷新
四十、购物车总价计算
例如:
总价:
商品1
+
商品2
+
商品3
很多人这样:
@State
total:number=0
然后:
手动:
total++
问题:
容易不同步。
推荐:
计算属性:
get total(){
let result=0
this.products.forEach(item=>{
result +=
item.price *
item.count
})
return result
}
页面:
Text(
this.cart.total.toString()
)
这样:
商品数量变化:
自动重新计算。
四十一、对象共享模型设计
大型项目:
不要:
每个页面创建对象。
错误:
HomePage
new User()
MinePage
new User()
OrderPage
new User()
三个User。
数据:
无法同步。
正确:
建立:
Model层。
例如:
common
|
models
|
User.ets
Order.ets
Message.ets
所有页面:
引用:
同一个对象。
结构:
AppStorage
|
UserStore
|
User
|
ObjectLink
|
Components
四十二、@Observed对象生命周期
这是面试重点。
例如:
@Observed
class User{
}
创建:
new User()
↓
构造函数
↓
建立观察能力
↓
进入状态树
↓
组件引用
销毁:
页面退出
↓
没有引用
↓
GC回收
注意:
@Observed:
不是:
全局对象。
它不会:
自动保存。
只是:
增强Class能力。
四十三、@Observed性能注意
虽然:
@Observed很好。
但是:
不要:
所有Model全部加。
错误:
所有Bean
↓
@Observed
后果:
大量监听。
推荐:
只给:
UI需要响应的数据。
例如:
服务器Response:
不要:
@Observed
class UserResponse{
100个字段
}
应该:
转换:
Response
↓
ViewModel
↓
Observed Model
↓
UI
四十四、企业架构推荐
大型HarmonyOS项目:
推荐:
Network
|
DTO
|
Repository
|
ViewModel
|
Observed Model
|
ArkUI Component
例如:
服务器:
{
"name":"Tom",
"age":18
}
DTO:
interface UserDTO{
name:string
age:number
}
转换:
UserModel
然后:
@Observed
class UserModel{
name:string
age:number
}
UI:
@ObjectLink
user:UserModel
四十五、本篇总结
本篇重点:
1. 企业项目为什么需要ObjectLink
因为:
大型页面:
需要:
精准刷新。
2. Chat列表最佳实践
Conversation
↓
Message
↓
ObjectLink
↓
ChatItem
3. 通讯录、购物车都适合
尤其:
对象数组。
4. ObjectLink不是复制
它是:
对象连接。
5. 推荐架构
DTO
↓
ViewModel
↓
@Observed Model
↓
@ObjectLink Component
四十六、ArkUI 状态管理到底监听什么?
在深入 @Observed 之前,需要先明确一个核心概念:
ArkUI 状态管理不是监听数据,而是监听“数据访问关系”。
什么意思?
例如:
Text(this.user.name)
ArkUI 并不是简单记录:
user.name
而是在构建阶段建立:
组件
↓
依赖
↓
user.name
关系。
也就是说:
当:
user.name
变化:
系统知道:
哪个组件依赖它。
然后:
只刷新相关组件。
例如
页面:
build(){
Column(){
Text(this.user.name)
Text(this.user.age)
}
}
系统内部类似:
Text1
依赖:
user.name
Text2
依赖:
user.age
如果:
user.name="Jack"
那么:
刷新:
Text1
而不是:
整个页面。
这就是 ArkUI 高性能的基础。
四十七、@Observed内部监听模型
我们看:
@Observed
class User{
name:string="Tom"
age:number=18
}
编译阶段:
ArkTS会处理:
User
↓
Observable能力
↓
属性变化通知
概念上类似:
class User {
private _name:string
get name(){
return this._name
}
set name(value:string){
this._name=value
notifyChange()
}
}
所以:
执行:
user.name="Jack"
不是简单赋值。
而是:
setter
↓
通知机制
↓
依赖更新
四十八、为什么修改普通对象不会触发?
看:
class User{
name:string="Tom"
}
执行:
user.name="Jack"
流程:
name修改
↓
JavaScript属性改变
↓
结束
没有:
通知
↓
组件
所以:
UI不知道。
而:
@Observed
class User{
}
流程:
name修改
↓
setter
↓
notify
↓
依赖组件
↓
刷新
四十九、@State数组 + @Observed对象组合
这是企业开发最常见组合。
例如:
@Observed
class User{
name:string=""
}
页面:
@State
users:Array<User>=[]
很多人认为:
完美。
但是:
这里有几个坑。
情况1:替换数组元素
this.users[0]=new User()
可以刷新。
因为:
数组结构变化。
情况2:修改对象属性
this.users[0].name="Jack"
是否刷新?
答案:
取决于:
Item是否使用:
@ObjectLink
错误:
ForEach(
this.users,
(item)=>{
Text(item.name)
}
)
可能:
刷新不符合预期。
正确:
拆组件。
父:
ForEach(
this.users,
(item)=>{
UserItem({
user:item
})
}
)
子:
@Component
struct UserItem{
@ObjectLink
user:User
build(){
Text(this.user.name)
}
}
这样:
稳定。
五十、为什么ForEach必须有key?
很多人:
写:
ForEach(
list,
item=>{
}
)
简单数据:
没问题。
复杂对象:
容易出现:
刷新错乱。
例如:
列表:
A
B
C
删除:
B。
如果没有key:
系统可能认为:
第2个位置
还是原来的组件
导致:
UI复用错误。
推荐:
ForEach(
this.users,
(item:User)=>{
UserItem({
user:item
})
},
item=>item.id.toString()
)
key作用:
告诉框架:
这个组件
对应哪个数据对象
五十一、List刷新错误案例
假设:
聊天列表:
[
{
id:1,
name:"Tom"
},
{
id:2,
name:"Jack"
}
]
页面:
Tom
Jack
插入:
[
{
id:3,
name:"Lucy"
},
{
id:1,
name:"Tom"
},
{
id:2,
name:"Jack"
}
]
没有key:
框架可能:
认为:
第一个组件
还是Tom
结果:
组件状态错乱。
有key:
id=3
创建新组件
id=1
复用旧组件
id=2
复用旧组件
性能和准确性都更好。
五十二、二维数组监听
企业项目经常有:
例如:
商品分类:
[
[
商品1,
商品2
],
[
商品3,
商品4
]
]
模型:
@State
goods:Array<Array<Product>>
问题:
修改:
goods[0][0].price=99
是否刷新?
取决于:
Product。
正确:
@Observed
class Product{
price:number=0
}
然后:
Item:
@ObjectLink
product:Product
完整监听链:
Array
↓
Array
↓
Product
↓
price
五十三、数组本身和数组元素是两回事
这是重点。
很多人混淆:
list.push()
和:
list[0].name=""
push
改变:
数组结构。
监听:
数组。
name修改
改变:
元素对象。
监听:
对象。
所以:
两个监听体系:
不同。
例如:
@State
users:Array<User>
负责:
增加
删除
排序
替换
@Observed
User
负责:
name
age
avatar
两者组合:
才能完整覆盖。
五十四、Map对象监听
例如:
缓存:
Map<number,User>
很多项目使用。
例如:
users.get(id)
修改:
users.get(1).name="Jack"
关键:
User必须:
@Observed
推荐:
@Observed
class User{
name:string=""
}
Map:
负责:
key变化
User:
负责:
属性变化
五十五、Set监听
例如:
权限集合:
Set<string>
变化:
add()
delete()
需要:
状态容器感知。
但是:
Set内部对象:
仍然需要:
Observed。
例如:
Set<User>
那么:
User:
必须:
@Observed
五十六、为什么服务器返回JSON不能直接绑定?
这是企业项目非常常见问题。
服务器:
{
"name":"Tom",
"address":{
"city":"Tokyo"
}
}
直接:
@State
user=response
然后:
修改:
user.name="Jack"
可能:
不刷新。
原因:
JSON解析出来:
只是:
普通对象。
不是:
Observed对象。
正确流程:
JSON
↓
DTO
↓
Model
↓
@Observed
↓
UI
例如:
class UserDTO{
name:string
}
转换:
let user =
new UserModel(dto)
五十七、makeV1Observed作用
HarmonyOS 新版本:
状态管理V1:
针对:
已有对象。
提供:
转换能力。
概念:
普通对象
↓
makeV1Observed
↓
可观察对象
适合:
场景:
已有大型项目。
例如:
后端Model:
class User{
}
改动成本太高。
不方便:
全部添加:
@Observed
可以:
运行时增强。
五十八、V1 和 V2 状态管理区别
HarmonyOS 状态管理经历:
两个阶段。
V1
典型:
@State
@Prop
@Link
@ObjectLink
@Observed
特点:
显式声明。
V2
新的:
@ObservedV2
@Trace
体系。
特点:
更细粒度。
例如:
@ObservedV2
class User{
@Trace
name:string=""
}
只追踪:
指定字段。
区别:
| V1 | V2 | |
|---|---|---|
| 监听粒度 | 对象级 | 属性级 |
| 学习成本 | 低 | 稍高 |
| 兼容性 | 高 | 新项目推荐 |
| ObjectLink | 需要 | 机制优化 |
五十九、什么时候使用 @Observed?
推荐:
以下情况:
✅ 页面共享对象
✅ 列表Item数据
✅ 复杂Model
✅ 局部刷新
✅ 多组件同步
不推荐:
❌ 网络DTO
❌ 临时对象
❌ 纯计算对象
六十、本篇总结
本篇核心:
1.
ArkUI监听的是:
数据访问关系
不是简单变量。
2.
@Observed负责:
对象属性变化通知。
3.
@ObjectLink负责:
组件订阅对象变化。
4.
数组监听和对象监听不同:
数组
负责结构变化
对象
负责属性变化
5.
企业项目推荐:
DTO
↓
Model(@Observed)
↓
Page(@State)
↓
Item(@ObjectLink)
↓
精准刷新
六十一、真实项目的数据流到底应该如何设计?
很多初学者的数据流通常是这样的:
网络请求
│
▼
Page页面
│
▼
List
│
▼
Item
例如:
this.userList = result.data
随后:
this.userList[0].name = "Tom"
页面开始出现:
- 有的刷新
- 有的不刷新
- List全部刷新
- Item重复创建
于是不断:
this.userList = [...this.userList]
或者:
this.userList = JSON.parse(JSON.stringify(this.userList))
很多项目甚至整个工程都是这样刷新。
实际上这是 React/Vue 的开发习惯,不是 ArkUI 推荐方式。
六十二、HarmonyOS 推荐的数据流
官方推荐的数据流应该是:
Repository
│
▼
Observed Model
│
▼
Page(@State)
│
▼
Item(@ObjectLink)
│
▼
ArkUI Render
举个例子。
网络返回:
{
"id":1,
"name":"Tom",
"age":18,
"avatar":"..."
}
不要直接:
this.user = response
推荐:
let model = new User()
model.id = dto.id
model.name = dto.name
model.age = dto.age
this.user = model
以后所有修改:
全部操作:
this.user
不要再操作DTO。
六十三、为什么要把 DTO 和 Model 分开?
这是很多大型公司的规范。
例如:
服务器:
{
"nickname":"Tom",
"vip_level":5,
"head_image":"xxx.png"
}
但是页面:
需要:
头像
昵称
VIP等级
如果直接使用:
head_image
整个项目:
都是:
user.head_image
以后服务器:
改成:
avatar
整个项目:
全部修改。
所以:
企业一般都会:
DTO
↓
Model
↓
UI
例如:
DTO:
interface UserDTO{
head_image:string
}
Model:
@Observed
class User{
avatar:string=""
}
转换:
user.avatar = dto.head_image
以后:
服务器怎么变。
UI不用改。
六十四、为什么大型项目都喜欢 ViewModel?
如果大家做过:
Android、
Flutter、
Compose、
SwiftUI
都会发现:
都有:
ViewModel
HarmonyOS同样推荐。
例如:
Repository
↓
UserViewModel
↓
User(@Observed)
↓
Page
↓
Component
ViewModel负责:
- 请求网络
- 转换数据
- 更新Model
- 通知页面
例如:
class UserViewModel{
user:User = new User()
}
收到网络:
this.user.name = dto.name
页面:
自动刷新。
六十五、ObjectLink 为什么适合聊天软件?
例如:
微信:
聊天:
500条。
每条:
头像
昵称
最后消息
未读数量
时间
如果:
收到:
Tom:
新消息。
真正变化:
只有:
最后消息
未读数量
ObjectLink:
刷新:
Tom这一行
其它:
499条:
不会刷新。
这就是:
局部刷新。
六十六、如果不用 ObjectLink 会怎样?
很多项目:
这样写:
ForEach(
this.list,
item=>{
Text(item.name)
}
)
收到消息:
this.list[10].lastMessage="Hello"
为了刷新:
很多人:
this.list=[...this.list]
结果:
500个Item
全部重新Build
如果:
每个Item:
还有:
Image
RichText
Badge
动画
CPU:
立刻升高。
ObjectLink:
流程:
修改:
Conversation
↓
通知:
ObjectLink
↓
刷新:
ConversationItem
↓
结束
真正:
只刷新:
一个。
六十七、购物车为什么推荐 ObjectLink?
购物车:
例如:
100件商品
点击:
+
只是:
count++
普通刷新:
整个购物车
↓
重新Build
100件商品
ObjectLink:
Product
↓
count变化
↓
当前Item刷新
效率:
差距非常明显。
六十八、ObjectLink 能不能替代 EventHub?
很多新人:
喜欢:
A页面
↓
EventHub.emit()
↓
B页面监听
其实:
很多情况:
完全不用。
例如:
共享:
User对象。
Mine
↓
User
↑
Home
↑
Message
三个页面:
ObjectLink。
修改:
user.avatar="xxx"
三个页面:
自动同步。
无需:
emit()
也无需:
on()
代码:
少很多。
六十九、ObjectLink 能不能跨页面?
这里:
很多人理解错。
ObjectLink:
本身:
不能跨页面。
它只是:
连接:
同一个对象引用。
真正:
跨页面:
依赖:
共享对象。
例如:
AppStorage
↓
UserStore
↓
User
↓
PageA
↓
PageB
两个页面:
拿到:
同一个:
User
ObjectLink:
自然同步。
七十、企业推荐 Store 设计
例如:
建立:
store
|
UserStore.ets
|
OrderStore.ets
|
MessageStore.ets
UserStore:
export class UserStore{
user:User=new User()
}
全局:
只有:
一个:
User
所有页面:
ObjectLink。
整个App:
数据一致。
七十一、ObjectLink 最大优势:组件自治
以前:
父组件:
需要:
updateChild()
或者:
callback()
现在:
Item:
自己监听。
例如:
@Component
struct UserItem{
@ObjectLink
user:User
}
父组件:
完全:
不用关心。
真正:
组件自治。
七十二、一个企业级错误案例
例如:
订单页面。
很多人:
点击:
确认收货
后台返回:
status=已完成
代码:
this.orders[index]=newOrder
整个List:
刷新。
实际上:
推荐:
this.orders[index].status="已完成"
Item:
ObjectLink:
自己刷新。
List:
不刷新。
七十三、性能测试对比
假设:
列表:
1000条。
每个Item:
头像
昵称
时间
消息
动画
修改:
一个Item。
方案一:
this.list=[...this.list]
结果:
1000
build
方案二:
item.name="Tom"
ObjectLink:
1
build
CPU:
相差:
几十倍。
这也是为什么 HarmonyOS 官方一直推荐对象级响应式,而不是频繁替换整个列表。
七十四、什么时候不要使用 @ObjectLink?
虽然它很好。
但是:
下面这些情况:
不推荐:
① 普通数字
@State
count:number
不用:
@ObjectLink
② 字符串
title:string
不用。
③ 临时变量
例如:
let index=0
不用。
④ 网络DTO
服务器返回:
{
"id":1
}
不要:
直接:
ObjectLink。
真正:
推荐:
Model
七十五、本章总结
企业项目:
最佳实践:
Network
↓
DTO
↓
Repository
↓
ViewModel
↓
Observed Model
↓
State(Page)
↓
ObjectLink(Item)
↓
Render
整个刷新:
始终:
围绕:
Model对象。
而不是:
数组。
不是:
JSON。
不是:
DTO。
更不是:
重新赋值。
七十六、先理解 ArkUI 响应式框架整体架构
很多开发者只知道:
this.user.name = "Tom"
页面刷新了。
但是:
真正发生的是一整套响应式流程。
如下图:
用户点击按钮
│
▼
this.user.name="Tom"
│
▼
@Observed Setter 被触发
│
▼
ObservableObject 通知变化
│
▼
SubscriberManager 查找订阅者
│
▼
找到所有 @ObjectLink Component
│
▼
标记组件 Dirty(MarkDirty)
│
▼
FrameScheduler 调度刷新
│
▼
Component Build()
│
▼
ArkUI Diff 比较
│
▼
RenderNode 更新
│
▼
GPU 渲染
很多文章讲到:
Setter
↓
刷新
其实:
中间还有很多步骤。
七十七、@Observed 底层设计思想
虽然官方没有公开完整源码,但根据 ArkUI 的工作机制,可以理解其设计思想。
普通对象:
class User {
name: string = ""
}
实际上只是:
对象
没有任何通知能力。
而:
@Observed
class User {
name: string = ""
}
可以理解为:
ObservableObject
│
├──── 属性1
├──── 属性2
├──── 属性3
│
SubscriberManager
也就是说:
每一个:
Setter
都会:
通知:
SubscriberManager
七十八、SubscriberManager 是什么?
可以理解为:
整个 ArkUI 的:
观察者中心
例如:
User对象
↓
SubscriberManager
↓
Child1
↓
Child2
↓
Child3
↓
Child4
只要:
User:
变化。
SubscriberManager:
立即:
通知:
Child1
Child2
Child3
Child4
进行刷新。
七十九、为什么不用事件通知?
有人问:
为什么不用:
EventHub.emit()
或者:
Emitter.emit()
原因:
事件:
属于:
广播
例如:
A页面
↓
emit()
↓
所有监听者收到
但是:
ObjectLink:
属于:
对象依赖
例如:
User
↓
Avatar组件
↓
Name组件
↓
Vip组件
修改:
User.avatar
只刷新:
依赖:
头像的组件。
而不是:
整个页面。
八十、MarkDirty 是什么?
HarmonyOS 渲染框架:
不会:
立即:
重新 build。
例如:
this.user.name = "Tom"
this.user.age = 18
this.user.avatar = "xxx"
如果:
每次:
都:
刷新。
那么:
Build()
Build()
Build()
性能:
很差。
实际上:
ArkUI:
先:
MarkDirty
例如:
UserItem
↓
Dirty = true
等:
这一帧:
统一:
刷新。
这就是:
批量更新。
八十一、Diff 为什么重要?
假设:
页面:
Column() {
Avatar()
Name()
Vip()
}
修改:
user.name = "Jack"
并不是:
整个:
Column
重建。
而是:
Diff:
发现:
Avatar
没变
↓
Vip
没变
↓
Name
变了
最终:
更新:
Text
不是:
整个页面。
八十二、为什么 ObjectLink 比 EventHub 性能高?
例如:
聊天:
1000条。
收到:
Tom:
消息。
EventHub:
流程:
emit()
↓
页面收到
↓
刷新List
↓
1000 Item
ObjectLink:
流程:
Conversation
↓
Subscriber
↓
ChatItem
↓
1个刷新
差距:
非常明显。
八十三、HarmonyOS 为什么没有像 Vue 那样全部 Proxy?
Vue:
整个:
Reactive()
↓
全部Proxy
HarmonyOS:
采用:
按需观察
例如:
只有:
@Observed
才:
建立:
观察能力。
原因:
大型项目:
对象:
可能:
几十万个。
全部Proxy:
初始化:
成本:
太高。
所以:
HarmonyOS:
采用:
显式声明。
八十四、HarmonyOS 5 状态管理 V2
HarmonyOS 5 开始:
推荐:
新的状态管理。
例如:
@ObservedV2
class User {
@Trace
name:string=""
}
以前:
V1:
整个对象:
通知。
现在:
V2:
可以:
指定:
Trace字段
例如:
name
↓
刷新
而:
cache
log
temp
不用:
监听。
粒度:
更细。
八十五、企业开发最佳实践
推荐:
项目目录:
model
│
├── User.ets
├── Message.ets
├── Order.ets
└── Product.ets
store
│
├── UserStore.ets
├── MessageStore.ets
└── OrderStore.ets
viewmodel
│
├── UserViewModel.ets
├── ChatViewModel.ets
└── OrderViewModel.ets
pages
│
├── HomePage
├── MinePage
└── OrderPage
components
│
├── UserItem
├── ChatItem
└── ProductItem
数据流:
Network
↓
Repository
↓
ViewModel
↓
Store
↓
Observed Model
↓
Page
↓
ObjectLink Component
这样:
整个项目:
职责清晰。
八十六、开发中最容易踩的 10 个坑
| 问题 | 原因 | 正确做法 |
|---|---|---|
| 修改对象属性不刷新 | 没有 @Observed |
给 Model 添加 @Observed |
| 子组件不刷新 | 没有 @ObjectLink |
使用 @ObjectLink 接收对象 |
| 嵌套对象不刷新 | 内层不是 @Observed |
每层需要可观察对象 |
| JSON 不刷新 | 普通对象 | 转换为 Model |
| List 全部刷新 | 替换整个数组 | 修改对象属性 |
| ForEach 错乱 | 没有 key | 使用稳定唯一 id |
| ObjectLink 无效 | 复制对象 | 保持对象引用 |
| DTO 与 UI 混用 | 数据职责混乱 | DTO → Model 转换 |
| Store 多实例 | 页面各自 new | 使用共享 Store |
| 滥用 EventHub | 所有页面广播 | 优先对象响应式 |
八十七、HarmonyOS 面试高频题(30 题精选)
1、@Observed 的作用是什么?
答案:
使普通 Class 成为可观察对象,当属性发生变化时,可以向订阅它的组件发送通知。
2、@ObjectLink 的作用是什么?
答案:
建立组件与 @Observed 对象之间的双向引用关系,使对象属性变化能够驱动组件刷新。
3、为什么 @Observed 不能单独使用?
答案:
@Observed 只负责发送对象变化通知,没有组件订阅这些通知时,页面不会刷新,需要 @ObjectLink 建立观察关系。
4、@ObjectLink 和 @Link 的区别?
答案:
| @Link | @ObjectLink |
|---|---|
| 基本类型/状态共享 | 可观察对象共享 |
| 面向变量 | 面向对象 |
| 不负责对象属性监听 | 支持对象属性监听 |
5、为什么修改 this.user.name 有时不刷新?
可能原因:
- User 未添加
@Observed - 子组件未使用
@ObjectLink - 使用普通 JSON 对象
- 中间嵌套对象未添加
@Observed
6、为什么 ForEach 一定要提供 key?
因为 ArkUI Diff 算法依赖稳定的 key 来复用组件,否则插入、删除或排序时容易出现组件状态错乱,并增加重建成本。
7、为什么推荐 Class 而不是 Object?
Class 可以:
- 被
@Observed修饰 - 封装业务逻辑
- 统一初始化
- 提供计算属性和方法
更适合企业级项目。
8、什么时候适合使用 @ObjectLink?
适用于:
- 聊天消息 Item
- 通讯录 Item
- 商品 Item
- 订单 Item
- 用户信息卡片
- 多组件共享同一对象
9、HarmonyOS V1 和 V2 状态管理有什么区别?
V1 以 @Observed + @ObjectLink 为核心。
V2 引入了更细粒度的状态追踪(如 @ObservedV2、@Trace),减少不必要的通知,更适合新项目。
10、ObjectLink 会复制对象吗?
不会。
它保存的是同一个对象引用,父子组件共同操作同一个实例,因此修改任意一方都会保持一致。
全文总结
经过整套系列,我们已经建立了完整的知识体系:
普通 Class
│
▼
@Observed
│
▼
可观察对象(Observable)
│
▼
@State 持有对象
│
▼
父组件传递引用
│
▼
@ObjectLink 建立订阅
│
▼
属性变化通知
│
▼
SubscriberManager
│
▼
MarkDirty
│
▼
Build
│
▼
Diff
│
▼
Render
企业项目最佳实践
建议遵循以下原则:
- DTO 与 UI Model 分离,不要直接把网络返回 JSON 绑定到 UI。
- 所有需要响应式更新的业务模型使用
@Observed。 - 列表 Item 使用
@ObjectLink,避免整个列表重建。 - 嵌套对象建立完整观察链,每层需要可观察能力。
- 使用稳定的
ForEachkey,保证 Diff 正确复用组件。 - 优先对象响应式,而不是频繁替换整个数组或使用事件广播。
更多推荐



所有评论(0)