网罗开发 (小红书、快手、视频号同名)

  大家好,我是 展菲,目前在上市企业从事人工智能项目研发管理工作,平时热衷于分享各种编程领域的软硬技能知识以及前沿技术,包括iOS、前端、Harmony OS、Java、Python等方向。在移动端开发、鸿蒙开发、物联网、嵌入式、云原生、开源等领域有深厚造诣。

图书作者:《ESP32-C3 物联网工程开发实战》
图书作者:《SwiftUI 入门,进阶与实战》
超级个体:COC上海社区主理人
特约讲师:大学讲师,谷歌亚马逊分享嘉宾
科技博主:华为HDE/HDG

我的博客内容涵盖广泛,主要分享技术教程、Bug解决方案、开发工具使用、前沿科技资讯、产品评测与使用体验。我特别关注云服务产品评测、AI 产品对比、开发板性能测试以及技术报告,同时也会提供产品优缺点分析、横向对比,并分享技术沙龙与行业大会的参会体验。我的目标是为读者提供有深度、有实用价值的技术洞察与分析。

展菲:您的前沿技术领航员
👋 大家好,我是展菲!
📱 全网搜索“展菲”,即可纵览我在各大平台的知识足迹。
📣 公众号“Swift社区”,每周定时推送干货满满的技术长文,从新兴框架的剖析到运维实战的复盘,助您技术进阶之路畅通无阻。
💬 微信端添加好友“fzhanfei”,与我直接交流,不管是项目瓶颈的求助,还是行业趋势的探讨,随时畅所欲言。
📅 最新动态:2025 年 3 月 17 日
快来加入技术社区,一起挖掘技术的无限潜能,携手迈向数字化新征程!


前言

最近在做鸿蒙应用开发的时候,发现 ArkUI 的状态管理机制很有意思。特别是 @ObservedV2 这个装饰器,用起来很简单,但底层实现其实挺复杂的。为什么修改一个被 @ObservedV2 标记的属性,UI 就能自动更新?这背后到底发生了什么?

今天我们就来聊聊 ObservedV2 的底层实现原理,看看鸿蒙是如何实现状态响应式更新的。理解这些底层机制,不仅能帮助我们更好地使用状态管理,还能在遇到性能问题或者奇怪的行为时,知道问题出在哪里。

ObservedV2 的基本使用

在深入底层实现之前,我们先看看 ObservedV2 是怎么用的:

@ObservedV2
class UserModel {
  name: string = "张三";
  age: number = 25;
  
  updateName(newName: string) {
    this.name = newName;
  }
}

@Builder
function UserView(user: UserModel) {
  Text(user.name)
    .fontSize(20)
}

看起来很简单,对吧?用 @ObservedV2 标记一个类,然后这个类的属性变化时,使用这些属性的 UI 就会自动更新。但这是怎么实现的呢?

响应式系统的核心机制

ObservedV2 的底层实现基于响应式系统的设计模式。简单来说,就是当被观察的属性发生变化时,系统会自动通知所有依赖这个属性的地方进行更新。

属性劫持机制

ObservedV2 的核心是属性劫持。当我们用 @ObservedV2 标记一个类时,系统会使用 Proxy 或者 Object.defineProperty 来劫持这个类的属性访问和修改。

// 伪代码:ObservedV2 的简化实现
function createObservedV2(target: any) {
  const propertyMap = new Map<string, Set<Function>>();
  
  return new Proxy(target, {
    get(target, prop) {
      // 收集依赖:记录哪些地方访问了这个属性
      track(target, prop);
      return target[prop];
    },
    
    set(target, prop, value) {
      const oldValue = target[prop];
      target[prop] = value;
      
      // 触发更新:通知所有依赖这个属性的地方
      if (oldValue !== value) {
        trigger(target, prop);
      }
      
      return true;
    }
  });
}

当我们访问 user.name 时,get 拦截器会被调用,系统会记录"当前正在执行的代码依赖 user.name"。当我们修改 user.name 时,set 拦截器会被调用,系统会通知所有依赖 user.name 的地方进行更新。

依赖收集过程

依赖收集是响应式系统的关键。系统需要知道哪些 UI 组件依赖哪些属性,这样当属性变化时,才能准确地更新对应的 UI。

// 伪代码:依赖收集的简化实现
let currentEffect: Function | null = null;

function track(target: any, prop: string) {
  if (currentEffect) {
    // 记录:currentEffect 依赖 target[prop]
    if (!propertyMap.has(prop)) {
      propertyMap.set(prop, new Set());
    }
    propertyMap.get(prop)!.add(currentEffect);
  }
}

function trigger(target: any, prop: string) {
  // 通知所有依赖这个属性的地方
  const effects = propertyMap.get(prop);
  if (effects) {
    effects.forEach(effect => effect());
  }
}

当 UI 组件渲染时,系统会设置 currentEffect 为这个组件的更新函数。然后当组件访问 user.name 时,track 函数会记录"这个组件的更新函数依赖 user.name"。当 user.name 变化时,trigger 函数会调用所有依赖 user.name 的更新函数,从而更新 UI。

UI 组件的响应式绑定

ArkUI 的 UI 组件是如何与响应式系统绑定的呢?当我们使用 @Builder 或者组件函数时,系统会创建一个响应式的更新函数:

// 伪代码:UI 组件的响应式绑定
function createReactiveComponent(builder: Function) {
  let updateFn: Function | null = null;
  
  return function render() {
    // 设置当前 effect
    currentEffect = () => {
      // 更新 UI 组件
      updateUI();
    };
    
    // 执行 builder,收集依赖
    const result = builder();
    
    // 清除当前 effect
    currentEffect = null;
    
    return result;
  };
}

当组件首次渲染时,系统会执行 render 函数。在 render 函数执行过程中,如果访问了 user.nametrack 函数会记录"这个组件的更新函数依赖 user.name"。当 user.name 变化时,系统会调用这个组件的更新函数,重新渲染 UI。

性能优化机制

响应式系统虽然强大,但如果实现不当,性能会是个大问题。ObservedV2 做了很多优化来保证性能。

批量更新机制

如果同时修改多个属性,系统不会立即触发更新,而是会批量处理:

// 伪代码:批量更新的简化实现
let updateQueue: Function[] = [];
let isUpdating = false;

function queueUpdate(effect: Function) {
  if (!updateQueue.includes(effect)) {
    updateQueue.push(effect);
  }
  
  if (!isUpdating) {
    isUpdating = true;
    // 使用微任务批量执行更新
    Promise.resolve().then(() => {
      updateQueue.forEach(effect => effect());
      updateQueue = [];
      isUpdating = false;
    });
  }
}

这样即使我们连续修改多个属性,系统也只会触发一次 UI 更新,大大提高了性能。

依赖去重

同一个组件可能多次访问同一个属性,系统会去重,避免重复的更新:

// 伪代码:依赖去重的简化实现
function track(target: any, prop: string) {
  if (currentEffect) {
    const effects = propertyMap.get(prop) || new Set();
    if (!effects.has(currentEffect)) {
      effects.add(currentEffect);
      propertyMap.set(prop, effects);
    }
  }
}

这样即使组件在渲染过程中多次访问 user.name,也只会记录一次依赖关系。

浅层响应式

ObservedV2 默认只对对象的直接属性进行响应式处理,不会递归处理嵌套对象:

@ObservedV2
class UserModel {
  name: string = "张三";
  address: Address = new Address(); // address 的变化不会触发更新
}

// 如果需要嵌套对象的响应式,需要单独标记
@ObservedV2
class Address {
  city: string = "北京";
}

这种设计可以避免不必要的性能开销,因为大多数情况下我们只需要响应直接属性的变化。

与旧版本 Observed 的区别

ObservedV2Observed 的升级版本,主要改进包括:

更细粒度的更新

Observed 在属性变化时会更新整个组件,而 ObservedV2 可以更精确地只更新依赖特定属性的部分:

// Observed:修改 name 会更新整个组件
@Observed
class UserModel {
  name: string = "张三";
  age: number = 25;
}

// ObservedV2:修改 name 只更新依赖 name 的部分
@ObservedV2
class UserModel {
  name: string = "张三";
  age: number = 25;
}

更好的性能

ObservedV2 使用了更高效的依赖收集和更新机制,性能比 Observed 更好,特别是在复杂场景下。

更灵活的 API

ObservedV2 提供了更灵活的 API,可以更好地控制响应式行为。

实际应用中的注意事项

理解了 ObservedV2 的底层实现,我们在实际开发中需要注意以下几点:

避免在循环中频繁修改属性

虽然系统有批量更新机制,但在循环中频繁修改属性仍然会影响性能:

// 不好的做法
for (let i = 0; i < 1000; i++) {
  user.name = `用户${i}`;
}

// 更好的做法
user.name = "最终值";

合理使用嵌套的 ObservedV2

如果需要嵌套对象的响应式,需要为每个嵌套对象单独标记 @ObservedV2

@ObservedV2
class UserModel {
  @ObservedV2
  address: Address = new Address();
}

注意内存泄漏

响应式系统会维护依赖关系,如果组件被销毁但依赖关系没有清理,可能会导致内存泄漏。不过 ArkUI 框架会自动处理这个问题,我们只需要确保组件正确销毁即可。

总结

ObservedV2 的底层实现基于响应式系统的设计模式,通过属性劫持、依赖收集、批量更新等机制,实现了高效的状态响应式更新。理解这些底层机制,可以帮助我们更好地使用状态管理,避免性能问题,写出更高效的代码。

虽然底层实现比较复杂,但使用起来很简单。我们只需要用 @ObservedV2 标记类,然后正常使用即可。框架会自动处理依赖收集和更新通知,让我们可以专注于业务逻辑的实现。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐