

前言
前面我们多次遇到「浅观察」的坑——@State cats: Cat[] = [],改 this.cats[0].y += 1 不触发重渲染,必须 this.cats = […this.cats] 整体赋值。数组项内部属性、类实例内部属性的变化,@State 都监听不到。实战中「改猫咪坐标」「改用户名」这种深属性变化是常态,每次都重新赋值整个数组/对象太笨。
HarmonyOS 提供了 @Observed + @ObjectLink 深观察方案——给类打 @Observed,子组件用 @ObjectLink 接收,改类实例内部属性就能触发重渲染。
本篇以「猫猫大作战」把 Cat 改成 @Observed 类、子组件深观察猫咪坐标为锚点,把 @Observed 类声明、@ObjectLink 子组件接收、深观察触发机制、与 @State 浅观察对比四大要点讲透。
提示:本系列不讲 ArkTS 基础语法与环境搭建,假设你已跟完第 1–42 篇。本篇是阶段二第十三篇,进入深观察深水区。
一、场景拆解:浅观察的深坑
回顾「猫猫大作战」猫咪下落(第 31、33 篇):
// 来源:entry/src/main/ets/pages/Index.ets 主循环
this.cats = this.gameEngine.updateCats(); // 引擎返回新数组
引擎内部(第 121 篇会专讲):
// 来源:entry/src/main/ets/components/GameEngine.ets updateCats()
sortedCats.forEach((cat) => {
const nextY = cat.y + 1;
if (nextY >= GameConfig.BOARD_HEIGHT) {
cat.falling = false;
this.placeCatOnBoard(cat);
} else if (this.board[nextY][cat.x] !== null) {
cat.falling = false;
this.placeCatOnBoard(cat);
} else {
cat.y = nextY; // ← 改类实例内部属性 y
}
});
return Array.from(this.cats.values());
痛点:引擎直接改 cat.y = nextY——这是改类实例内部属性。UI 层的 @State cats: Cat[] 浅观察,监听不到这种深变化。
当前解法(第 31 篇):引擎返回新数组,UI 整体赋值 this.cats = …,靠数组引用变化触发重渲染。但每个猫只移动一格,就要重建整个数组 diff,浪费。
@Observed + @ObjectLink 的解法:
// 1. Cat 改成 @Observed 类
@Observed
export class Cat {
id: string;
level: CatLevel;
x: number;
y: number;
falling: boolean;
constructor(id: string, level: CatLevel, x: number, y: number, falling: boolean) {
this.id = id;
this.level = level;
this.x = x;
this.y = y;
this.falling = falling;
}
}
// 2. 子组件用 @ObjectLink 深观察
@Component
struct CatItem {
@ObjectLink cat: Cat; // 深观察,cat.y 改了触发重渲染
build() {
Column() { /* … */ }
.position({ x: this.cat.x * CELL, y: this.cat.y * CELL })
}
}
关键经验:@Observed + @ObjectLink 让「改类实例内部属性」直接触发重渲染——不用整体赋值数组。
二、@Observed 类声明
2.1 给类打 @Observed
@Observed
export class Cat {
id: string;
level: CatLevel;
x: number;
y: number;
falling: boolean;
constructor(id: string, level: CatLevel, x: number, y: number, falling: boolean) {
this.id = id;
this.level = level;
this.x = x;
this.y = y;
this.falling = falling;
}
}
拆解:
| @Observed | 装饰器,标记「本类的实例可被深观察」 |
| export class Cat | 普通类,有字段和构造器 |
关键约束:@Observed 必须打在 class 上——不能打 interface。interface 是类型契约无运行时实例,@Observed 要靠类的实例化才能生效。
2.2 interface vs class 的取舍
回顾原 GameTypes.ets(第 15 篇):
// 原 interface
export interface Cat {
id: string;
level: CatLevel;
x: number;
y: number;
falling: boolean;
}
改造成 @Observed 类后:
@Observed
export class Cat {
id: string;
level: CatLevel;
x: number;
y: number;
falling: boolean;
constructor(id: string, level: CatLevel, x: number, y: number, falling: boolean) {
this.id = id;
this.level = level;
this.x = x;
this.y = y;
this.falling = falling;
}
}
| 运行时实例 | ❌ 类型契约 | ✅ 有构造器创建实例 |
| 深观察 | ❌ | ✅ 改内部属性触发 |
| 语法 | 简 | 略繁(要 constructor) |
| 适合 | 纯类型契约 | 要深观察的数据类 |
关键经验:纯类型契约用 interface,要深观察的数据类用 @Observed class——Cat、User、ComboInfo 这种会改内部属性的,都适合 @Observed。
2.3 实例化必须用 new
// interface 方式(旧)
const cat: Cat = { id: 'cat_0', level: CatLevel.SMALL, x: 0, y: 0, falling: true };
// @Observed class 方式(新)
const cat: Cat = new Cat('cat_0', CatLevel.SMALL, 0, 0, true);
``
**关键经验**:**@Observed 类实例化必须用 `new`**——对象字面量 `{}` 不走构造器,不会被 @Observed 标记深观察。
## 三、@ObjectLink 子组件接收
### 3.1 子组件声明 @ObjectLink
```ts
@Component
struct CatItem {
@ObjectLink cat: Cat; // 深观察 Cat 实例
build() {
Column() {
Text(/* emoji */)
}
.width(/* size */)
.position({ x: this.cat.x * CELL_SIZE, y: this.cat.y * CELL_SIZE })
// ← 读 cat.x/cat.y,深变化自动重渲染
}
}
拆解:
| @ObjectLink | 装饰器,标记「深观察父传的 @Observed 实例」 |
| cat | 变量名 |
| Cat | 类型(必须是 @Observed class) |
关键约束:@ObjectLink 的类型必须是 @Observed class——传 interface 或普通 class 无深观察效果。
3.2 父组件传 @Observed 实例
@Entry
@Component
struct Index {
@State cats: Cat[] = []; // 数组里是 @Observed Cat 实例
build() {
ForEach(this.cats, (cat: Cat) => {
CatItem({ cat: cat }) // 传实例给子组件
}, (cat: Cat) => cat.id)
}
}
传递语法:与 @Prop 一样传普通值,但子用 @ObjectLink 接收会深观察。
3.3 @ObjectLink vs @Prop
| 接收类型 | 任意 | 必 @Observed class |
| 观察 | 浅(引用变) | 深(内部属性变) |
| 父改数组项 | ✅ 触发(引用变) | ✅ 触发 |
| 改内部属性 | ❌ 不触发 | ✅ 触发 |
| 适合 | 普通数据 | @Observed 实例 |
关键经验:@ObjectLink 是 @Prop 的「深观察版」——专用于 @Observed 类实例,改内部属性就触发重渲染。
四、深观察触发机制
4.1 改内部属性直接触发
// 子组件 CatItem
@Component
struct CatItem {
@ObjectLink cat: Cat;
build() {
Column() { /* … */ }
.position({ x: this.cat.x * CELL, y: this.cat.y * CELL })
}
}
// 引擎改 cat.y
cat.y = nextY; // ← 改 @Observed 实例内部属性
// ArkUI:
// 1. cat.y 赋值
// 2. 检测到 cat 是 @Observed 实例,有 @ObjectLink 依赖
// 3. 标记所有依赖 cat.y 的 CatItem 为脏
// 4. 下一帧重渲染,position 新值
关键经验:@Observed + @ObjectLink 让「改 cat.y」直接触发重渲染——不用重新赋值整个数组。
4.2 深观察的依赖追踪
@Component
struct CatItem {
@ObjectLink cat: Cat;
build() {
Column()
.position({ x: this.cat.x * CELL, y: this.cat.y * CELL })
// build 时 ArkUI 记录:依赖 cat.x 和 cat.y
}
}
机制:
关键经验:深观察也靠依赖图——只是依赖追踪到「内部属性」层级,而非浅观察的「引用」层级。
4.3 多子组件观察同一实例
// 多个 CatItem 观察同一只猫
ForEach(this.cats, (cat: Cat) => {
CatItem({ cat: cat }) // CatItem A
})
// 假设还有预览组件也观察同一只猫
CatPreview({ cat: this.cats[0] }) // CatPreview B
// 引擎改 this.cats[0].y
// A 和 B 都重渲染(都依赖 cat.y)
实战经验:@Observed 实例被多个 @ObjectLink 观察时,改属性触发所有依赖者——这是「一处改多处刷」的广播机制。
五、改造猫猫大作战用 @Observed
5.1 Cat 改成 @Observed class
修改 entry/src/main/ets/components/GameTypes.ets:
// 改造:interface Cat → @Observed class Cat
@Observed
export class Cat {
id: string;
level: CatLevel;
x: number;
y: number;
falling: boolean;
constructor(id: string, level: CatLevel, x: number, y: number, falling: boolean) {
this.id = id;
this.level = level;
this.x = x;
this.y = y;
this.falling = falling;
}
}
// 其他类型保持 interface(不需要深观察的)
export interface ComboInfo {
count: number;
multiplier: number;
lastMergeTime: number;
}
5.2 引擎用 new 实例化
修改 entry/src/main/ets/components/GameEngine.ets:
import { Cat, CatLevel, GameConfig, ComboInfo } from './GameTypes';
export class GameEngine {
private board: (Cat | null)[][] = [];
private cats: Map<string, Cat> = new Map();
/* … */
dropCat(column: number): boolean {
if (column < 0 || column >= GameConfig.BOARD_WIDTH) return false;
if (this.board[0][column] !== null) return false;
// 改造:用 new Cat() 替代对象字面量
const cat: Cat = new Cat(
`cat_${this.catCounter++}`,
this.nextCatLevel,
column,
0,
true
);
this.cats.set(cat.id, cat);
this.nextCatLevel = this.randomLevel();
return true;
}
// tryMergeAt 内合并生成新猫也用 new
private tryMergeAt(x: number, y: number): boolean {
/* … */
if (sameLevelNeighbors.length >= GameConfig.MERGE_COUNT) {
const newLevel = (cat.level + 1) as CatLevel;
// 改造:new Cat()
const newCat: Cat = new Cat(
`cat_${this.catCounter++}`,
newLevel,
x,
y,
false
);
/* … */
}
}
}
5.3 创建 CatItem 子组件用 @ObjectLink
新建 entry/src/main/ets/components/CatItem.ets:
import { Cat, CatConfig } from './GameTypes';
@Component
export struct CatItem {
@ObjectLink cat: Cat; // 深观察猫咪实例(本篇重点)
build() {
Column() {
Text(CatConfig[this.cat.level].emoji)
.fontSize(CatConfig[this.cat.level].size * 0.5)
}
.width(CatConfig[this.cat.level].size)
.height(CatConfig[this.cat.level].size)
.borderRadius(CatConfig[this.cat.level].size / 2)
.backgroundColor(CatConfig[this.cat.level].color)
.justifyContent(FlexAlign.Center)
.shadow({ radius: 4, color: 'rgba(0,0,0,0.2)', offsetY: 2 })
.position({
x: this.cat.x * 60 + (60 – CatConfig[this.cat.level].size) / 2, // ← 读 cat.x
y: this.cat.y * 60 + (60 – CatConfig[this.cat.level].size) / 2 // ← 读 cat.y
})
.animation({ duration: 100, curve: Curve.Linear })
}
}
5.4 Index 用 CatItem 替代内联渲染
// 来源:entry/src/main/ets/pages/Index.ets GameView() 棋盘 Stack 改造
import { CatItem } from '../components/CatItem';
@Builder
GameView() {
Column() {
this.GameHUD()
Column() {
Row() { /* 预告区 */ }
Stack() {
// 棋盘背景
Column() { /* ForEach rows × cols */ }
// 猫咪渲染:改用 CatItem 子组件
ForEach(this.cats, (cat: Cat) => {
CatItem({ cat: cat }) // ← 传 @Observed 实例
}, (cat: Cat) => cat.id)
// 列点击层
Row() { /* ForEach cols onClick */ }
}
.width(GameConfig.BOARD_WIDTH * GameConfig.CELL_SIZE)
.height(GameConfig.BOARD_HEIGHT * GameConfig.CELL_SIZE)
.borderRadius(12).clip(true).backgroundColor('#D6EEF5')
}.alignItems(HorizontalAlign.Center)
Spacer()
Row() { /* 底部控制栏 */ }
.width('100%').padding({ left: 24, right: 24, bottom: 24, top: 12 })
}
.width('100%').height('100%')
.linearGradient({
direction: GradientDirection.Bottom,
colors: [['#E8F4F8', 0.0], ['#D6EEF5', 0.5], ['#C9E8F2', 1.0]]
})
.alignItems(HorizontalAlign.Center)
}
5.5 改造对比
| cat.y 改变 | ❌ 不触发,要整体赋值数组 | ✅ 直接触发 |
| 数组重建 | 每 100ms 重建整个数组 | 不用,改属性即可 |
| 性能 | N 只猫整体 diff | 只重渲染移动的那只 |
| 代码 | 内联 Column position | 子组件 CatItem 复用 |
| 实例化 | 对象字面量 {} | new Cat() |
六、踩坑提示
6.1 忘给 class 打 @Observed
// ❌ 错误:没 @Observed,@ObjectLink 退化为浅观察
class Cat { /* … */ }
@Component
struct CatItem {
@ObjectLink cat: Cat; // 不会深观察
}
// ✅ 正确:@Observed
@Observed
export class Cat { /* … */ }
6.2 用 interface 而非 class
// ❌ 错误:interface 不能打 @Observed
@Observed
export interface Cat { /* … */ } // 编译报错
// ✅ 正确:用 class
@Observed
export class Cat { /* … */ }
6.3 对象字面量实例化
// ❌ 错误:对象字面量不走构造器,不被 @Observed 标记
const cat: Cat = { id: '0', level: CatLevel.SMALL, x: 0, y: 0, falling: true };
// ✅ 正确:new 实例化
const cat: Cat = new Cat('0', CatLevel.SMALL, 0, 0, true);
6.4 @ObjectLink 类型不是 @Observed class
// ❌ 错误:@ObjectLink 接 interface,无深观察
export interface ComboInfo { /* … */ }
@Component
struct ComboView {
@ObjectLink combo: ComboInfo; // interface 无深观察
}
// ✅ 正确:@ObjectLink 接 @Observed class
@Observed
export class ComboData { /* … */ }
@Component
struct ComboView {
@ObjectLink combo: ComboData;
}
6.5 多个 @ObjectLink 改同一实例属性竞态
// 多个子组件观察同一只猫
CatItem({ cat: this.cats[0] }) // A
CatPreview({ cat: this.cats[0] }) // B
// 引擎改 this.cats[0].y
// A 和 B 都重渲染——无竞态,都是读新值
关键经验:深观察是「改后广播」,不是「改中共享」——无竞态问题。
七、调试技巧
八、性能与最佳实践
九、V1 vs V2 预告
本篇讲的 @Observed + @ObjectLink 是 V1 状态管理的深观察方案。HarmonyOS 还提供 V2 的 @ObservedV2 + @Trace(本系列第 50 篇会专讲),区别:
| 装饰器 | @Observed class + @ObjectLink 子 | @ObservedV2 class + @Trace 字段 |
| 子组件接收 | @ObjectLink | 普通 @Prop(V2 自深观察) |
| 性能 | 中 | 高(优化依赖追踪) |
| 推荐 | 当前稳定 | 新项目首选 |
关键经验:V1 @Observed+@ObjectLink 当前稳定兼容好,V2 @ObservedV2+@Trace 是未来方向——新项目可直接上 V2。
总结
本篇我们从 @Observed+@ObjectLink 深观察切入,掌握了**@Observed 类声明(必 class 非 interface)、@ObjectLink 子组件接收**、改内部属性直接触发深观察、与 @State 浅观察对比四大要点,并给出了 Cat 改成 @Observed class、CatItem 子组件深观察的完整改造代码。核心要点:要深观察用 @Observed class;@ObjectLink 接 @Observed;实例化必须 new;改属性直接触发省整体 diff。
下一篇我们将拆解 @Observed 数组项替换——arr[i]=newItem 的深观察。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- 「猫猫大作战」项目源码:本仓库 entry/src/main/ets/components/GameTypes.ets、entry/src/main/ets/components/GameEngine.ets、entry/src/main/ets/components/CatItem.ets
- ArkUI @Observed/@ObjectLink 深观察官方指南
- ArkUI 状态管理概述
- ArkUI 深观察与性能最佳实践
- 开源鸿蒙跨平台社区
- HarmonyOS 开发者官方文档首页
- 系列索引:本仓库 articles/INDEX.md






