news 2026/9/21 15:35:41

Valdi 组件状态管理实战:ViewModel 单向数据流与 StatefulComponent 的 setState 机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Valdi 组件状态管理实战:ViewModel 单向数据流与 StatefulComponent 的 setState 机制
  • 跨平台
  • UI组件
  • 前端
  • 移动开发

【免费下载链接】Valdi

Valdi is a cross-platform UI framework that delivers native performance without sacrificing developer velocity.

项目地址:https://gitcode.com/gh_mirrors/val/Valdi
点击查看免费下载

本文是 Valdi 快速上手 Codelab 系列的核心章节(对应仓库 docs/codelabs/getting_started/5-component_state.md),深入讲解 Valdi 组件在脱离热重载(hotreloader)后如何自主更新 UI:父组件通过ViewModel向下传递不可变数据,子组件通过StatefulComponentsetState()管理内部状态。读完本文,你将掌握 ViewModel 接口的定义与传参、State 的初始化与局部更新、定时器驱动的状态刷新及生命周期清理,并能理解这些 API 在 Valdi 运行时中的底层实现原理。

从热重载到生产环境:组件为什么需要"真正的状态"

在前面的章节中,我们一直使用局部变量配合热重载工具来刷新界面:改一行代码,热重载工具重编译模块,应用随之重新渲染。但一个组件一旦进入生产环境运行,就没有"热重载"替它刷新视图了——此时,状态的变更必须由组件自己感知并触发重渲染

Valdi 的答案是:当特定种类的状态发生变化时,自动重新渲染组件。为此,每个组件拥有两种主要状态:

  • ViewModel:由父组件注入的数据,向子组件单向流动;
  • State:组件自身的内部状态,由组件自己维护和更新。

下面分别展开。

ViewModel 与单向数据流

在 Valdi 中,数据沿组件树自上而下传播:父组件通过ViewModel把数据传给子组件。对于子组件而言,ViewModel 是**不可变(immutable)**的——只能读取其中的变量,不能修改它。当父组件更新子组件的 ViewModel 时,子组件可以在下一次onRender中拿到新值,也可以在生命周期回调onViewModelUpdate中感知这次更新。

第一步:定义 ViewModel 接口

ViewModel 被定义为 TypeScript 接口,并作为泛型参数指定在Component上。在GettingStartedCodelab组件定义之外、imports 之下定义:

export interface GettingStartedCodeLabViewModel { header: string; }

推荐命名规范为NameOfComponentViewModel(组件名 + ViewModel 后缀)。

第二步:把 ViewModel 指定为 Component 的泛型

export class GettingStartedCodelab extends Component<GettingStartedCodeLabViewModel> {

第三步:在 onRender 中消费 ViewModel

onRender() { const fullMsg = this.msg + new Date().toLocaleTimeString(); <layout> <label value={this.viewModel.header} font={systemBoldFont(16)} /> {this.onRenderMessage(fullMsg)} </layout>; }

this.viewModel.header直接读取父组件注入的值。从源码看,Component基类把 ViewModel 保存为只读属性:readonly viewModel: Readonly<ViewModel>,在构造函数中由渲染器传入(见 Component.ts)。Readonly<T>保证了类型层面的不可变性——子组件只能读,不能写。

第四步:父组件必须传入参数,否则编译报错

此时如果让热重载刷新,你会看到报错或缺失的 header。原因在于:我们没有在父组件添加<GettingStartedCodelab>的位置定义header参数。打开 HelloWorldApp.tsx,在onRender中找到<GettingStartedCodelab>所在位置(初始组件定义见 GettingStartedCodelab.tsx)。

VS Code 会给出红色波浪线,悬停后提示:

"Property 'header' is missing in type '{}' but required in type 'Readonly<GettingStartedCodeLabViewModel>'"

这是因为父组件没有把header值传进子组件的 ViewModel。修复方法是为header标签补上属性值:

<GettingStartedCodelab header='State codelab'></GettingStartedCodelab>

热重载刷新后,新 header 就会出现。

可选字段:不是所有 ViewModel 变量都必须传

如果某些 ViewModel 变量并非每次都需要,可在接口定义中将其设为可选(optional):

export interface GettingStartedCodeLabViewModel { header: string; numKittens?: number; }

numKittens?是可选的,父组件传不传都不会报错。这与 TypeScript 的可选属性语义一致,也是 ViewModel 驱动类型安全 UI 的典型用法。

State:组件的内部状态

State对象用于追踪组件的内部状态。与 ViewModel 类似,State 也是不可变的;但与 ViewModel 不同的是,它可以通过组件的setState()函数被修改,而setState()会触发组件重新渲染。

第一步:定义 State 接口

紧跟在 ViewModel 之后定义:

interface GettingStartedCodeLabState { elapsed: number; kitten: boolean; }

State 接口不需要export,因为它不会在本文件之外使用——这正是"内部状态"语义的体现。

第二步:改用 StatefulComponent

Valdi 提供了专门处理 State 的组件基类。把组件改为继承StatefulComponent,并同时指定 ViewModel 与 State 两个泛型:

import { StatefulComponent } from "valdi_core/src/Component"; /** * @Component */ export class GettingStartedCodelab extends StatefulComponent< GettingStartedCodeLabViewModel, GettingStartedCodeLabState > {

从源码可以看到,StatefulComponent继承自Component,在基类之上增加了state?: Readonly<State>属性和setState()方法(见 Component.ts)。@Component注释用于标记 Valdi 编译器需要处理的组件类。

第三步:初始化 state 变量

/** * @Component */ export class GettingStartedCodelab extends StatefulComponent< GettingStartedCodeLabViewModel, GettingStartedCodeLabState > { state = { elapsed: 0, kitten: false, };

这里直接为state字段赋初始值,作为组件创建时的默认内部状态。

第四步:用定时器驱动状态更新

现在让elapsed状态"动起来"。在onCreate中设置一个定时器,并用interval属性保存其引用:

private interval?: number; onCreate() { this.msg = 'Hello valdi on '; this.kitten = true; this.interval = setInterval(() => { this.setState({ elapsed: this.state.elapsed + 1 }); }, 1000); }

这里涉及两个关键概念:

  • setInterval是 JavaScript 提供的定时器工具,会在固定延迟后重复调用指定函数。第一个参数是要调用的函数——这里是一个 lambda,每秒钟调用一次setStateelapsed加 1;第二个参数是毫秒数(1000ms = 1 秒)。
  • setState做的是局部更新(partial update):传入的只是状态对象的子集,Valdi 会把新值与旧状态合并,而不是整体替换。因此这里只更新elapsedkitten的值保持不变。

从源码看,setState的合并逻辑由mergePartial实现(见 PartialUtils.ts):它会遍历 partial 中的每个键,只把发生变化(oldValue !== newValue)的字段复制到新对象中,其余字段原样保留;若没有任何字段变化,则返回undefined,不会触发无谓的重渲染。setState在合并出新状态后调用scheduleRender()同步调度组件重新渲染(见 Component.ts)。

第五步:在 onDestroy 中清理定时器

setInterval会永远运行下去,因此组件销毁时必须清理,否则会造成泄漏:

onDestroy() { if (this.interval) { clearInterval(this.interval); } }

clearInterval清除由setInterval创建的定时器;onDestroy是 Component 的生命周期函数,在组件被销毁时调用。作为对照,源码中setState在组件已销毁时会在控制台输出错误警告,提示"请确保在 onDestroy 中取消所有订阅和异步任务,否则可能造成内存泄漏"(见 Component.ts)——这正是本小节要规避的问题。Valdi 还提供了更便捷的setTimeoutDisposable,它创建的定时器会在组件销毁时自动失效(见 Component.ts)。

第六步:在 onRender 中读取 State

onRender中再添加一个<label>,显示不断增长的elapsed

onRender() { const fullMsg = this.msg + new Date().toLocaleTimeString(); <layout> <label value={this.viewModel.header} font={systemBoldFont(16)} /> <label value={`Time Elapsed: ${this.state.elapsed} seconds`} /> {this.onRenderMessage(fullMsg)} </layout>; }

注意这里的 value 是模板字符串(template literal):以反引号(back tick,`)包裹、用${}插值,而不是普通引号。让热重载刷新后,就能看到秒数逐秒递增——UI 已经由状态驱动自动更新,不再依赖热重载。

第七步:用状态驱动条件渲染

再使用另一个 State 变量kitten。把setInterval里的更新逻辑改为基于elapsed % 5计算:

this.interval = setInterval(() => { this.setState({ elapsed: this.state.elapsed + 1, kitten: this.state.elapsed % 5 == 0, }); }, 1000);

这样每当经过 5 秒的倍数时,kitten会被置为true。注意这里同时更新了两个字段——由于setState是局部合并,即使一次传入多个字段,也只会合并变化的部分,不会影响其他状态。

然后在onRenderMessage中消费state.kitten,并清理原来的 kitten 局部变量:

onRenderMessage(message: string) { if (this.state.kitten) { <image height={50} width={50} src='https://placecats.com/300/300' />; } else { <label value={message} font={systemFont(12)} />; } }

运行后观察 UI:大多数时间显示 hello 消息,每隔 5 秒切换显示一张小猫图片。至此,一个由setState驱动的完整状态循环已经跑通:状态变化 → 触发重渲染 → 读取新状态 → 更新 UI → 再次变化

结合生命周期理解状态流

onCreateonRenderonDestroy这些回调的调用时机,与 ViewModel/State 的流转密切相关。根据配套章节 6-component_lifecycle.md 及源码注释(见 Component.ts),可以总结如下:

  • onCreate:组件首次创建时调用一次,此时 ViewModel 与 State 已就绪但尚未开始渲染,适合初始化局部变量、启动定时器等一次性工作;
  • onViewModelUpdate(previousViewModel?):ViewModel 被设置、或父组件在最近一次onRender后更新了它时调用;若父组件在初始化时提供了 ViewModel,它会在onCreate之后立刻被调用,且其后总会紧跟一次onRender
  • onRender:渲染入口,只有在该函数调用栈内的 TSX 标签才会生效。它有三种触发场景——onCreate之后的首次渲染、onViewModelUpdate之后、以及StatefulComponent调用setState之后;
  • onDestroy:组件被销毁时调用一次(例如父组件不再在onRender中渲染它),是清理长任务与资源的正确位置。

也就是说,ViewModel 的更新走"父传子 +onViewModelUpdate"通道,State 的更新走"setState+ 重渲染"通道,二者最终都汇聚到onRender,这正是 Valdi 声明式 UI 的核心循环。

更多进阶:动画化状态变更与测试注入

StatefulComponent还提供了两个在源码中可见的进阶 API,值得一提:

  • setStateAnimated(state, animationOptions)/setStateAnimatedPromise(...):与setState语义相同(局部合并、同步重渲染),但会把本次状态变更引发的元素变化关联到动画上,动画结束时可获得 Promise 通知(见 Component.ts)。适合需要"数字跳动、列表平滑过渡"等场景。
  • 测试中的 ViewModel 注入:在编写组件单元测试时,可以通过createComponent(GettingStartedCodelab, { header: 'Something' })直接注入 ViewModel 来验证渲染结果,相关用法详见 8-unittest.md。

小结

  • ViewModel是父组件向子组件单向传递数据的通道,子组件只读不可写,通过泛型Component<ViewModel>声明,父组件必须提供全部必填字段;
  • State是组件内部状态,通过StatefulComponent<ViewModel, State>setState()维护;setState执行局部合并(底层为mergePartial),仅更新发生变化的字段,并同步触发重渲染;
  • onCreate / onRender / onDestroy等生命周期回调与 ViewModel/State 的更新通道共同构成了 Valdi 的声明式渲染循环;长任务必须在onDestroy中清理,setTimeoutDisposable可自动完成这一职责。

掌握了 ViewModel 与 State 的配合,你的 Valdi 组件就具备了在生产环境中自主响应数据变化的能力。下一步可以继续阅读 6-component_lifecycle.md,深入理解组件生命周期各阶段的完整时序。

  • 跨平台
  • UI组件
  • 前端
  • 移动开发

【免费下载链接】Valdi

Valdi is a cross-platform UI framework that delivers native performance without sacrificing developer velocity.

项目地址:https://gitcode.com/gh_mirrors/val/Valdi
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/21 15:33:55

从 VuePress 迁移到 VitePress:侧边栏配置与图片处理改造全指南

前端文档 【免费下载链接】vitepress Vite & Vue powered static site generator. 项目地址&#xff1a; https://gitcode.com/gh_mirrors/vi/vitepress 点击查看 免费下载 本指南以 VitePress 官方迁移文档为主线&#xff0c;系统讲解从 VuePress 迁移到 VitePress 时最容…

作者头像 李华
网站建设 2026/9/21 15:33:49

file_selector_web:Flutter Web 文件选择器的类型过滤机制与演进史

file_selector_web&#xff1a;Flutter Web 文件选择器的类型过滤机制与演进史 【免费下载链接】plugins Plugins for Flutter maintained by the Flutter team 项目地址: https://gitcode.com/gh_mirrors/pl/plugins file_selector_web 是 Flutter 官方插件 file_selec…

作者头像 李华
网站建设 2026/9/21 15:33:37

012_效率与线性度之间的电路折中

012、效率与线性度之间的电路折中 一个让我赔了两周调试时间的效率陷阱 前年做一个电池供电的便携式数据采集设备,前级传感器输出是微伏到毫伏级的缓慢变化信号,后级要驱动一个无线发射模块。系统要求整机平均功耗低于某个硬指标,因为电池容量小,客户又要求连续工作几十个…

作者头像 李华