HarmonyOS鸿蒙Next中@ObservedV2与@Trace装饰的对象key都带有__ob_

HarmonyOS鸿蒙Next中@ObservedV2@Trace装饰的对象key都带有__ob_ 这种侵入性是不是太强了? , 按论坛说的 如果从json字符串反序列化回来还得转对象, 而且@Trace修饰的字段都丢失了. 这种问题有啥好的解决方案么? 本来有要转json存到本地的需求. 转json后 @Trace修饰的字段都带_ob_前缀, 然后反序列化转对象后, @Trace的字段属性全都没了.

6 回复

__ob_前缀无法避免,且前缀不影响对象属性的getter和setter方法使用。如果涉及序列化与反序列化,可通过如下示例方法,将前缀去除。

创建一个新的类,类中属性名和原来的对象相同,用原来对象的值来初始化新类的对象。

[@ObservedV2](/user/ObservedV2)
class FormDataClassV2 {
  @Trace name: string = '默认名称';
  @Trace price: number = 0;
}

class FormDataClass {
  name: string = '';
  price: number = 0;

  constructor(v: FormDataClassV2) {
    this.name = v.name;
    this.price = v.price;
  }
}

@Entry
@ComponentV2
struct FormDataClassPage {
  @Local data: FormDataClassV2 = new FormDataClassV2();

  build() {
    Column() {
      Button('序列化')
        .onClick(() => {
          console.info('序列化原始值:', JSON.stringify(this.data));
          console.info('序列化转换后:', JSON.stringify(new FormDataClass(this.data)));
        });
    }
    .height('100%')
    .width('100%');
  }
}

【背景知识】

状态管理V2装饰器会为装饰的变量生成getter和setter方法,同时为原有变量名添加__ob_的前缀,当前无法避免,详情请参考官方文档:获取状态管理V2代理前的原始对象

@ObservedV2的类实例目前不支持使用JSON.stringify进行序列化,详情请参考使用限制

更多关于HarmonyOS鸿蒙Next中@ObservedV2与@Trace装饰的对象key都带有__ob_的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


开发者你好,关于 @ObservedV2 嵌套对象监听的问题,已经有小伙伴给出了正确的说明。这里补充一些深层次的原理说明。

原理说明

状态管理 V2 的代理机制

状态管理 V2 通过代理(Proxy)机制来实现深度监听。当使用 [@ObservedV2](/user/ObservedV2)[@Trace](/user/Trace) 装饰器时,框架会为被装饰的对象创建一个代理对象,这个代理对象会拦截属性的访问和修改操作,从而实现响应式更新。

getTarget 接口的作用

在开发中,有时需要获取代理前的原始对象(例如传递给第三方库、序列化等场景)。所以提供了 getTarget() 接口来获取状态管理框架代理前的原始对象。

使用场景

  • 需要将对象传递给不支持代理的第三方库
  • 需要序列化对象时获取原始数据
  • 调试时需要查看原始对象的状态

参考文档获取状态管理V2代理前的原始对象

@ObservedV2@Trace 的使用限制

在使用 [@ObservedV2](/user/ObservedV2)[@Trace](/user/Trace) 时,需要注意以下限制:

  1. 必须配合使用[@ObservedV2](/user/ObservedV2) 必须与 [@Trace](/user/Trace) 配合使用才能实现属性观测
  2. 类级别装饰[@ObservedV2](/user/ObservedV2) 只能装饰类,不能装饰接口或类型别名
  3. 属性级别装饰[@Trace](/user/Trace) 只能装饰被 [@ObservedV2](/user/ObservedV2) 装饰的类中的属性
  4. 嵌套对象处理:对于嵌套对象,需要确保嵌套的对象也被 [@ObservedV2](/user/ObservedV2) 装饰

参考文档:[@ObservedV2@Trace装饰器使用限制](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-new-observedv2-and-trace#使用限制)

深入理解

如果想更深入地理解状态管理 V2 的代理机制、响应式更新原理以及在实际开发中的最佳实践,可以参考以下我所写的博客文章:

参考文档

重新new 一遍

当使用@ObservedV2@Trace装饰类时,框架会在属性名前添加__ob_前缀来实现响应式追踪:

[@ObservedV2](/user/ObservedV2)
class User {
  [@Trace](/user/Trace) id: number = 1;
  [@Trace](/user/Trace) name: string = 'Tom';
}

const user = new User();
console.log(JSON.stringify(user));
// 输出: {"__ob_id":1,"__ob_name":"Tom"}
// 而不是: {"id":1,"name":"Tom"}

问题影响

  1. 序列化污染: JSON字符串包含__ob_前缀
  2. 反序列化失败: JSON.parse()后对象失去响应式能力
  3. 本地存储异常: Preferences/DataStore存储的数据格式错误
  4. 网络请求问题: 后端API无法识别带前缀的字段
  5. 路由传参异常: router.pushUrl()传递对象显示undefined

解决方案汇总

方案一:自定义序列化/反序列化工具类(推荐)

// utils/SerializationHelper.ets
export class SerializationHelper {
  /**
   * 序列化[@ObservedV2](/user/ObservedV2)对象为纯净JSON
   * 自动移除__ob_前缀
   */
  static serialize<T>(obj: T): string {
    const cleaned = this.cleanObservedObject(obj);
    return JSON.stringify(cleaned);
  }

  /**
   * 反序列化JSON为[@ObservedV2](/user/ObservedV2)对象
   * 恢复响应式能力
   */
  static deserialize<T>(jsonStr: string, classConstructor: new () => T): T {
    const plainObj = JSON.parse(jsonStr);
    return this.restoreObservedObject(plainObj, classConstructor);
  }

  /**
   * 清理__ob_前缀
   */
  private static cleanObservedObject(obj: any): any {
    if (obj === null || typeof obj !== 'object') {
      return obj;
    }
    if (Array.isArray(obj)) {
      return obj.map(item => this.cleanObservedObject(item));
    }
    const cleaned: Record<string, any> = {};
    for (const key in obj) {
      if (obj.hasOwnProperty(key)) {
        // 移除__ob_前缀
        const cleanKey = key.startsWith('__ob_') ? key.substring(5) : key;
        cleaned[cleanKey] = this.cleanObservedObject(obj[key]);
      }
    }
    return cleaned;
  }

  /**
   * 恢复为[@ObservedV2](/user/ObservedV2)实例
   */
  private static restoreObservedObject<T>(plainObj: any, classConstructor: new () => T): T {
    const instance = new classConstructor();
    for (const key in plainObj) {
      if (plainObj.hasOwnProperty(key)) {
        // 处理嵌套对象
        if (plainObj[key] && typeof plainObj[key] === 'object' && !Array.isArray(plainObj[key])) {
          // 如果属性本身是[@ObservedV2](/user/ObservedV2)类,需要递归恢复
          // 这里简化处理,直接赋值
          (instance as any)[key] = plainObj[key];
        } else if (Array.isArray(plainObj[key])) {
          // 处理数组
          (instance as any)[key] = plainObj[key].map((item: any) => {
            if (item && typeof item === 'object') {
              return item; // 简化处理,实际应递归
            }
            return item;
          });
        } else {
          // 基础类型直接赋值
          (instance as any)[key] = plainObj[key];
        }
      }
    }
    return instance;
  }

  /**
   * 保存到本地存储
   */
  static async saveToPreferences<T>(
    preferences: any,
    key: string,
    obj: T
  ): Promise<void> {
    const jsonStr = this.serialize(obj);
    await preferences.put(key, jsonStr);
    await preferences.flush();
  }

  /**
   * 从本地存储读取
   */
  static async loadFromPreferences<T>(
    preferences: any,
    key: string,
    classConstructor: new () => T,
    defaultValue: T
  ): Promise<T> {
    try {
      const jsonStr = await preferences.get(key, '');
      if (!jsonStr) {
        return defaultValue;
      }
      return this.deserialize(jsonStr, classConstructor);
    } catch (error) {
      console.error(`Load from preferences failed: ${error}`);
      return defaultValue;
    }
  }
}

使用示例

import { SerializationHelper } from '../utils/SerializationHelper';
import { preferences } from '@kit.ArkData';

[@ObservedV2](/user/ObservedV2)
class UserSettings {
  [@Trace](/user/Trace) id: number = 0;
  [@Trace](/user/Trace) name: string = '';
  [@Trace](/user/Trace) age: number = 0;
  [@Trace](/user/Trace) darkMode: boolean = false;
  [@Trace](/user/Trace) tags: string[] = [];
}

@Entry
@ComponentV2
struct SettingsPage {
  @Local settings: UserSettings = new UserSettings();
  private dataPreferences: preferences.Preferences | null = null;

  async aboutToAppear() {
    // 初始化Preferences
    const context = getContext(this);
    this.dataPreferences = await preferences.getPreferences(context, 'user_settings');
    // 从本地加载设置
    this.settings = await SerializationHelper.loadFromPreferences(
      this.dataPreferences,
      'settings',
      UserSettings,
      new UserSettings()
    );
  }

  async saveSettings() {
    if (!this.dataPreferences) return;
    // 保存到本地
    await SerializationHelper.saveToPreferences(
      this.dataPreferences,
      'settings',
      this.settings
    );
    console.info('Settings saved successfully');
  }

  build() {
    Column() {
      Text(`Name: ${this.settings.name}`)
      Text(`Age: ${this.settings.age}`)
      Text(`Dark Mode: ${this.settings.darkMode}`)
      Button('Update Settings')
        .onClick(() => {
          this.settings.name = 'Alice';
          this.settings.age = 25;
          this.settings.darkMode = true;
          this.settings.tags.push('developer');
        })
      Button('Save to Local Storage')
        .onClick(() => {
          this.saveSettings();
        })
    }
  }
}

方案二:使用toJSON自定义序列化(官方推荐)

虽然官方说不支持JSON.stringify,但可以通过实现toJSON()方法自定义序列化行为:

[@ObservedV2](/user/ObservedV2)
class UserProfile {
  [@Trace](/user/Trace) id: number = 0;
  [@Trace](/user/Trace) name: string = '';
  [@Trace](/user/Trace) email: string = '';

  /**
   * 自定义JSON序列化
   * JSON.stringify会自动调用此方法
   */
  toJSON(): Record<string, any> {
    return {
      id: this.id,
      name: this.name,
      email: this.email
    };
  }

  /**
   * 从JSON恢复实例
   */
  static fromJSON(json: string): UserProfile {
    const obj = JSON.parse(json);
    const instance = new UserProfile();
    instance.id = obj.id;
    instance.name = obj.name;
    instance.email = obj.email;
    return instance;
  }
}

// 使用示例
const user = new UserProfile();
user.id = 1;
user.name = 'Bob';
user.email = 'bob@example.com';

// 序列化(会调用toJSON方法)
const jsonStr = JSON.stringify(user);
console.log(jsonStr); // {"id":1,"name":"Bob","email":"bob@example.com"}

// 反序列化
const restored = UserProfile.fromJSON(jsonStr);
console.log(restored.name); // "Bob"

方案三:Router传参专用解决方案(官方示例)

官方文档提供了router传参的解决方案(第986-1016行):

// pages/Index.ets
[@ObservedV2](/user/ObservedV2)
export class RouterModel {
  [@Trace](/user/Trace) id: number = 0;
  [@Trace](/user/Trace) info: string = '';
  [@Trace](/user/Trace) tags: string[] = [];

  /**
   * 序列化为router参数(移除__ob_前缀)
   */
  toRouterParams(): Record<string, any> {
    return {
      id: this.id,
      info: this.info,
      tags: [...this.tags]
    };
  }

  /**
   * 从router参数恢复
   */
  static fromRouterParams(params: any): RouterModel {
    const instance = new RouterModel();
    instance.id = params.id || 0;
    instance.info = params.info || '';
    instance.tags = params.tags || [];
    return instance;
  }
}

@Entry
@ComponentV2
struct IndexPage {
  @Local model: RouterModel = new RouterModel();

  onJumpClick() {
    this.model.id = 1;
    this.model.info = 'Test Data';
    this.model.tags = ['tag1', 'tag2'];
    // 正确方式:序列化为纯净对象
    this.getUIContext().getRouter().pushUrl({
      url: 'pages/DetailPage',
      params: this.model.toRouterParams() // 使用toRouterParams方法
    });
  }

  build() {
    Column() {
      Button('Jump to Detail')
        .onClick(() => this.onJumpClick())
    }
  }
}

// pages/DetailPage.ets
import { RouterModel } from './Index';

@Entry
@ComponentV2
struct DetailPage {
  @Local model: RouterModel = new RouterModel();

  aboutToAppear() {
    const params = this.getUIContext().getRouter().getParams();
    // 正确方式:恢复为[@ObservedV2](/user/ObservedV2)实例
    this.model = RouterModel.fromRouterParams(params);
  }

  build() {
    Column() {
      Text(`ID: ${this.model.id}`)
      Text(`Info: ${this.model.info}`)
      Text(`Tags: ${this.model.tags.join(', ')}`)
    }
  }
}

方案四:使用Proxy拦截JSON操作

高级方案,通过Proxy自动处理序列化:

export class ObservedV2Proxy {
  /**
   * 创建支持自动序列化的代理对象
   */
  static create<T extends object>(instance: T): T {
    return new Proxy(instance, {
      get(target, prop) {
        // 拦截JSON.stringify的toJSON调用
        if (prop === 'toJSON') {
          return () => {
            const cleaned: Record<string, any> = {};
            for (const key in target) {
              if (target.hasOwnProperty(key)) {
                const cleanKey = key.startsWith('__ob_') ? key.substring(5) : key;
                cleaned[cleanKey] = (target as any)[key];
              }
            }
            return cleaned;
          };
        }
        return (target as any)[prop];
      }
    });
  }
}

// 使用示例
[@ObservedV2](/user/ObservedV2)
class Product {
  [@Trace](/user/Trace) id: number = 0;
  [@Trace](/user/Trace) name: string = '';
  [@Trace](/user/Trace) price: number = 0;
}

const product = ObservedV2Proxy.create(new Product());
product.id = 1;
product.name = 'Laptop';
product.price = 999;

const json = JSON.stringify(product);
console.log(json); // {"id":1,"name":"Laptop","price":999}

完整实战示例:持久化用户配置

// models/AppConfig.ets
import { SerializationHelper } from '../utils/SerializationHelper';

[@ObservedV2](/user/ObservedV2)
export class ThemeConfig {
  [@Trace](/user/Trace) colorMode: number = 0; // 0: light, 1: dark
  [@Trace](/user/Trace) fontSize: number = 14;
  [@Trace](/user/Trace) language: string = 'zh-CN';
}

[@ObservedV2](/user/ObservedV2)
export class AppConfig {
  [@Trace](/user/Trace) version: string = '1.0.0';
  [@Trace](/user/Trace) theme: ThemeConfig = new ThemeConfig();
  [@Trace](/user/Trace) lastLoginTime: number = Date.now();

  toJSON(): Record<string, any> {
    return {
      version: this.version,
      theme: {
        colorMode: this.theme.colorMode,
        fontSize: this.theme.fontSize,
        language: this.theme.language
      },
      lastLoginTime: this.lastLoginTime
    };
  }

  static fromJSON(json: string): AppConfig {
    const obj = JSON.parse(json);
    const config = new AppConfig();
    config.version = obj.version;
    config.theme.colorMode = obj.theme?.colorMode || 0;
    config.theme.fontSize = obj.theme?.fontSize || 14;
    config.theme.language = obj.theme?.language || 'zh-CN';
    config.lastLoginTime = obj.lastLoginTime || Date.now();
    return config;
  }
}

// managers/ConfigManager.ets
import { preferences } from '@kit.ArkData';
import { AppConfig } from '../models/AppConfig';

export class ConfigManager {
  private static instance: ConfigManager;
  private dataPreferences: preferences.Preferences | null = null;
  private config: AppConfig = new AppConfig();

  private constructor() {}

  static getInstance(): ConfigManager {
    if (!ConfigManager.instance) {
      ConfigManager.instance = new ConfigManager();
    }
    return ConfigManager.instance;
  }

  async init(context: any): Promise<void> {
    this.dataPreferences = await preferences.getPreferences(context, 'app_config');
    await this.load();
  }

  async load(): Promise<void> {
    if (!this.dataPreferences) return;
    try {
      const jsonStr = await this.dataPreferences.get('config', '');
      if (jsonStr) {
        this.config = AppConfig.fromJSON(jsonStr as string);
        console.info('Config loaded successfully');
      }
    } catch (error) {
      console.error(`Load config failed: ${error}`);
    }
  }

  async save(): Promise<void> {
    if (!this.dataPreferences) return;
    try {
      const jsonStr = JSON.stringify(this.config); // 会调用toJSON()
      await this.dataPreferences.put('config', jsonStr);
      await this.dataPreferences.flush();
      console.info('Config saved successfully');
    } catch (error) {
      console.error(`Save config failed: ${error}`);
    }
  }

  getConfig(): AppConfig {
    return this.config;
  }
}

// EntryAbility.ets
import { ConfigManager } from '../managers/ConfigManager';

export default class EntryAbility extends UIAbility {
  async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
    // 初始化配置管理器
    await ConfigManager.getInstance().init(this.context);
  }
}

// pages/SettingsPage.ets
import { ConfigManager } from '../managers/ConfigManager';

@Entry
@ComponentV2
struct SettingsPage {
  @Local config: AppConfig = ConfigManager.getInstance().getConfig();

  build() {
    Column() {
      Text(`Version: ${this.config.version}`)
      Text(`Color Mode: ${this.config.theme.colorMode === 0 ? 'Light' : 'Dark'}`)
        .onClick(() => {
          this.config.theme.colorMode = this.config.theme.colorMode === 0 ? 1 : 0;
        })
      Text(`Font Size: ${this.config.theme.fontSize}`)
      Slider({
        value: this.config.theme.fontSize,
        min: 12,
        max: 24,
        step: 1
      })
        .onChange((value: number) => {
          this.config.theme.fontSize = Math.round(value);
        })
      Button('Save Settings')
        .onClick(async () => {
          await ConfigManager.getInstance().save();
          promptAction.showToast({ message: 'Settings saved!' });
        })
    }
    .padding(20)
  }
}

方案对比

方案 优点 缺点 适用场景
自定义工具类 通用性强 自动处理前缀 支持嵌套对象 需要额外代码 复杂数据持久化
toJSON方法 符合JS规范 代码清晰 每个类都要实现 手动映射字段 简单对象序列化
Router专用 官方推荐 明确语义 仅适用Router 页面间传参
Proxy拦截 自动化程度高 性能开销 调试困难 高级场景

最佳实践建议

  1. 推荐使用toJSON方法 - 简单清晰,符合标准
  2. 复杂嵌套用工具类 - 减少重复代码
  3. Router传参用专用方法 - 避免反序列化问题
  4. 持久化统一管理 - 使用Manager类封装
  5. 文档化序列化逻辑 - 团队协作必备

在HarmonyOS鸿蒙Next中,@ObservedV2@Trace装饰器用于状态管理。@ObservedV2装饰的类对象会被自动观察属性变化,而@Trace用于标记需要跟踪的响应式属性。两者装饰的对象在内部实现中会添加__ob_前缀的键,用于系统识别和管理响应式数据。这有助于框架在属性变更时触发UI更新,无需手动处理依赖追踪。

在HarmonyOS Next中,@ObservedV2@Trace装饰器会在对象属性key上添加__ob_前缀,这是框架实现响应式追踪的机制,确实会带来序列化/反序列化的挑战。针对你的问题,可以考虑以下方案:

  1. 自定义序列化方法:在转为JSON前,手动过滤掉__ob_前缀的属性。例如使用JSON.stringifyreplacer参数或类自定义toJSON方法,排除这些内部属性。

  2. 数据拷贝策略:在序列化前,通过浅拷贝或深拷贝创建一个纯净的对象副本,仅保留原始数据字段,避免装饰器添加的元数据。

  3. 反序列化后重新装饰:从JSON解析后,手动对需要追踪的字段重新应用@Trace装饰器,确保响应式特性恢复。虽然增加步骤,但能保持数据与装饰逻辑一致。

这些方法需要额外处理,但能平衡框架特性和存储需求。如果字段较多,建议封装工具函数统一处理,减少重复代码。

回到顶部