本文同步发表于我的微信公众号,微信搜索 程语新视界 即可关注,每个工作日都有文章更新

鸿蒙(HarmonyOS)中 @State 的底层更新机制涉及 响应式数据绑定依赖收集差异更新(Diff) 等多个环节。以下是更底层的技术解析,结合 ArkUI 框架的实现逻辑和简化版源码流程:

一、核心更新流程(分步骤拆解)

1. 阶段一:状态初始化(编译时)

当使用 @State 修饰变量时,ArkTS 编译器会生成对应的 ObservedProperty 子类的实例,并注入到组件中:

// 开发者编写的代码
@State count: number = 0;

// 编译后的等效代码(伪代码)
private __count: ObservedPropertySimple<number> = 
    new ObservedPropertySimple(0, this, "count");

// ObservedPropertySimple 的简化实现
class ObservedPropertySimple<T> {
  private value_: T;
  private subscribers_: Component[] = []; // 订阅该属性的组件

  set value(newVal: T) {
    if (this.value_ !== newVal) {
      this.value_ = newVal;
      this.notifyChange(); // 触发更新
    }
  }

  notifyChange() {
    this.subscribers_.forEach(comp => comp.markNeedRender());
  }
}
2. 阶段二:依赖收集(运行时)

当组件首次渲染时,框架会记录哪些 UI 组件依赖了该状态(自动建立变量与UI组件的订阅关系):

// 伪代码:Text 组件读取 count 时的逻辑
Text(`${this.count}`) 
// 底层实际调用:
// 1. 调用 this.count 的 getter
// 2. 将当前 Text 组件注册到 __count.subscribers_ 中
3. 阶段三:状态变更触发更新

当状态被修改时(如 this.count = 1),触发以下流程:

// 伪代码:setter 调用链
this.count = 1 
→ __count.value = 1 
→ __count.notifyChange() 
→ 遍历 subscribers_ 中的组件
→ 每个组件调用 markNeedRender()

变量被修改时(如 this.count = 1),会触发 propertyChange 事件,通知所有订阅的UI组件重新渲染。 

4. 阶段四:UI 差异更新(Diff + Patch)

框架会对比新旧虚拟 DOM(VDOM),仅更新变化的部分:

二、关键底层技术点

1. 依赖收集的实现
  • 订阅关系存储:每个 ObservedProperty 实例维护一个 subscribers_ 数组,存储依赖它的组件。
  • 动态绑定:在组件 build() 过程中,每当读取 @State 变量时,自动将当前组件加入订阅列表。
2. 异步批量更新

鸿蒙采用与 React 类似的异步更新策略:

// 伪代码:批量更新逻辑
function setState(newValue) {
  if (!isBatchingUpdates) {
    // 直接触发更新
    applyUpdate();
  } else {
    // 加入更新队列
    pendingUpdates.push(newValue);
  }
}

// 事件回调中的批量处理
onClick(() => {
  isBatchingUpdates = true;
  this.count++; // 不会立即更新
  this.count++; // 不会立即更新
  isBatchingUpdates = false;
  flushPendingUpdates(); // 统一处理
});

异步批量更新: 多次连续修改会合并为一次UI更新(类似React的setState) 

3. VDOM Diff 算法

ArkUI 的 Diff 算法优先比较同层级节点,通过以下策略优化性能:

// 伪代码:简化版 Diff
function diff(oldVNode, newVNode) {
  if (oldVNode.type !== newVNode.type) {
    return replaceNode(oldVNode, newVNode); // 类型不同直接替换
  }
  patchProps(oldVNode.props, newVNode.props); // 更新属性
  diffChildren(oldVNode.children, newVNode.children); // 递归子节点
}

差分更新(Diff): 框架会对比新旧VDOM,仅更新变化的节点。 

三、与主流框架的底层对比

技术点 ArkUI (@State) React (useState) Vue (reactive)
响应式原理 编译时生成 ObservedProperty 闭包 + 强制更新 Proxy 拦截
依赖收集 运行时动态绑定 渲染时重新收集 Proxy getter 拦截
更新粒度 组件级 组件级 组件级
Diff算法 同级比较 + Key 优化 双端比较 + Fiber 动态规划优化

四、实际案例

示例:点击按钮更新计数
@Entry
@Component
struct Counter {
  @State count: number = 0;

  build() {
    Column() {
      Text(`${this.count}`) // 文本订阅count
        .fontSize(30)
      Button("+1")
        .onClick(() => {
          this.count++; // 触发更新
        })
    }
  }
}

底层执行流程

  1. 初始化Text 组件读取 count 时,将自身注册到 __count.subscribers_
  2. 点击事件
    • this.count++ 调用 __count.value 的 setter
    • 触发 notifyChange()
    • 标记 Text 组件需要更新
  3. 渲染帧
    • 重新执行 Counter.build()
    • 生成新的 Text VDOM
    • Diff 发现只有文本内容变化
    • 仅更新 Text 节点的 textContent 属性

五、扩展(批量检查(Batching Updates)核心机制)

        批量检查(Batching Updates) 是优化状态更新性能的核心机制,其原理类似于 React 的 setState 批量更新或 Vue 的异步更新队列。以下是其工作原理的深度解析:

批量检查的核心目标

  • 减少渲染次数:将短时间内多次状态变更合并为一次 UI 更新。
  • 避免中间状态渲染:确保连续操作只触发最终状态的渲染。

实现原理(分步骤拆解)

1. 更新队列的维护

@State 变量被修改时,框架不会立即触发 UI 更新,而是将变更放入一个 待处理队列pendingUpdates):

// 伪代码:简化版更新队列
let isBatching = false;
let pendingUpdates: Function[] = [];

function scheduleUpdate(updateFn: Function) {
  if (isBatching) {
    pendingUpdates.push(updateFn); // 加入队列
  } else {
    updateFn(); // 立即执行
  }
}
2. 批量更新的触发时机

鸿蒙在以下场景会启动批量模式:

  • 事件回调:如 onClickonTouch 等交互事件。
  • 生命周期钩子:如 aboutToAppearonPageShow
  • 异步任务:如 setTimeout/Promise 回调(需特殊处理)。
// 伪代码:事件回调的批量处理
function wrapEvent(eventFn: Function) {
  return () => {
    isBatching = true; // 开启批量模式
    eventFn();         // 执行用户代码
    isBatching = false;
    flushPendingUpdates(); // 统一处理队列
  };
}

Button("Click").onClick(wrapEvent(() => {
  this.count++; // 加入队列
  this.total++; // 加入队列
}));
3. 队列的消费(Flush)

批量模式下,所有更新会被合并为一次 build() 调用:

function flushPendingUpdates() {
  if (pendingUpdates.length === 0) return;
  
  // 执行所有更新函数
  pendingUpdates.forEach(fn => fn());
  pendingUpdates = [];

  // 触发单次UI更新
  currentComponent.markNeedRender();
}

关键设计细节

1. 异步更新策略

鸿蒙使用类似 JavaScript 事件循环的机制,将更新推迟到 下一渲染帧 前执行:

 

2. 更新优先级
  • 同步更新:部分高优先级操作(如动画)可能绕过批量检查。
  • 防抖控制:连续快速操作会被合并(如滚动事件)。
3. 与React/Vue的对比
框架 批量检查触发时机 异步更新方式
鸿蒙 ArkUI 事件回调、生命周期 下一渲染帧
React 事件回调、生命周期 微任务(Microtask)
Vue 数据变更、$nextTick 微任务

实际案例验证

示例:连续多次更新
@State count: number = 0;

Button("快速增加").onClick(() => {
  this.count++; // 1
  this.count++; // 2
  this.count++; // 3
  // 最终只触发一次渲染,count直接变为3
});

控制台输出

[渲染前] count值: 0
[渲染后] count值: 3  // 跳过了中间状态1和2

特殊场景处理

1. 异步代码中的更新

默认情况下,异步回调(如 setTimeout不启用批量检查:

// 以下会触发两次渲染
setTimeout(() => {
  this.count++; // 第一次渲染
  this.count++; // 第二次渲染
}, 1000);

解决方案:手动包裹批量上下文

import { ActionScope } from '@ohos.ability.common';

ActionScope.runBatch(() => {
  setTimeout(() => {
    this.count++; // 加入批量队列
    this.count++;
  }, 1000);
});
2. 跨组件更新

父子组件间的状态更新会被统一批量处理:

// 父组件
@State data = 0;

// 子组件
@Prop childData: number;
Button("修改").onClick(() => {
  this.parent.updateData(); // 父组件更新
  this.localData++;         // 子组件本地状态
  // 合并为一次渲染
})

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐