欢迎光临
我们一直在努力

HarmonyOS应用开发实战:猫猫大作战-`Cat` 改成 `@Observed` 类、子组件深观察猫咪坐标为锚点,把 @Observed 类声明、

文章配图: 改成  类、子组件深观察猫咪坐标为锚点,把 @Observed 类声明、

页面预览

前言

前面我们多次遇到「浅观察」的坑——@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;
}
}

维度interface@Observed class
运行时实例 ❌ 类型契约 ✅ 有构造器创建实例
深观察 ✅ 改内部属性触发
语法 略繁(要 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

维度@Prop@ObjectLink
接收类型 任意 必 @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
}
}

机制:

  • 首次 build(),ArkUI 读 this.cat.x 和 this.cat.y,记录依赖。
  • 当 cat.x 或 cat.y 被改,查依赖图,标记本 CatItem 为脏。
  • 下一帧重渲染,position 用新值。
  • 关键经验:深观察也靠依赖图——只是依赖追踪到「内部属性」层级,而非浅观察的「引用」层级。

    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 改造对比

    维度原 @State 浅观察版@Observed+@ObjectLink 版
    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 都重渲染——无竞态,都是读新值

    关键经验:深观察是「改后广播」,不是「改中共享」——无竞态问题。

    七、调试技巧

  • console.info 在子组件 build 首行:log this.cat.x, this.cat.y,追深观察触发。
  • 改属性不刷新排查:检查类是否 @Observed;检查子是否 @ObjectLink;检查是否 new 实例化。
  • DevEco ArkUI Inspector:查看 @Observed 实例的依赖图。
  • 整体赋值还触发:@ObjectLink 也监听引用变,所以整体赋值数组仍触发——只是改属性也触发(双保险)。
  • 八、性能与最佳实践

  • 要深观察的数据类用 @Observed class——Cat、User、ComboData 等会改内部属性的。
  • 纯类型契约用 interface——GameState、CatLevel enum 等不改的。
  • @ObjectLink 接 @Observed class——接 interface 或普通 class 无深观察。
  • 实例化必须用 new——对象字面量不走构造器,不被 @Observed 标记。
  • 改内部属性直接触发——不用整体赋值数组,省 N 只猫整体 diff。
  • 多处观察同一实例广播——改属性所有依赖者重渲染,无竞态。
  • 九、V1 vs V2 预告

    本篇讲的 @Observed + @ObjectLink 是 V1 状态管理的深观察方案。HarmonyOS 还提供 V2 的 @ObservedV2 + @Trace(本系列第 50 篇会专讲),区别:

    维度V1 @Observed+@ObjectLinkV2 @ObservedV2+@Trace
    装饰器 @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
    赞(0)
    未经允许不得转载:171主机测评 » HarmonyOS应用开发实战:猫猫大作战-`Cat` 改成 `@Observed` 类、子组件深观察猫咪坐标为锚点,把 @Observed 类声明、
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址