TypePHP\Qt
指南
控件
深入
参考
FAQ
  • English
  • 简体中文
GitHub
指南
控件
深入
参考
FAQ
  • English
  • 简体中文
GitHub
  • 深入

    • 深入
    • AOT 注意事项
    • 手写桥接
    • diff 引擎
    • 无头验收

diff 引擎

这一页解释为什么控件状态能在重渲染后保留 —— 这是声明式方案最核心的机制。

问题

view() 每帧执行一次(约 60 次/秒),每次都返回一棵全新的控件树。

如果每帧都"把旧控件全删、按新树重建",会发生什么?

  • 你正在输入的文字光标跳回开头
  • 表格的选中行消失
  • 滚动位置弹回顶部
  • 每秒 60 次重建 200 个控件 —— 卡

所以必须差异更新。

做法:按 id diff

C++ 侧维护一个 id → 控件 的表。每帧收到新树后:

新树里有的 id,旧表里也有    → 只更新变化的属性(控件本身不动)
新树里有、旧表里没有的 id    → 建控件
旧表里有、新树里没有的 id    → 删控件(先从布局摘除,再 deleteLater)

控件本身不重建,所以它的内部状态(光标、选中、滚动、输入法状态)全部原样保留。

稳定 id 是前提

diff 认的是 id,所以 id 必须跨帧稳定 —— 同一个逻辑控件,这一帧和下一帧要拿到同一个 id。

显式 id

你写的 ['id' => 'name_input'] 就是它。

自动 id:结构路径

没写 id 的节点,C++ 按结构位置生成:_p0.1.2(第 0 个根的第 1 个子的第 2 个子)。

因为是从位置推的,只要树的结构不变,同一个节点每帧拿到同一个 id。

反例:id 每帧都变

这是真实踩过的坑

早期实现给无 id 节点用一个自增计数器生成 id。结果:每帧 id 都不同 → diff 认为"旧的全消失、全是新的" → 每帧重建整棵树。

后果不只是慢:重建时旧控件被删,而布局里还留着指向它的悬垂指针 —— 第二次渲染直接崩(0xC0000005)。

教训:自动 id 必须从结构推导,不能从执行顺序推导。

选中的保留

表格/列表/树的选中是个特例:用户点选后,选中状态在控件里,不在你的状态里。

diff 引擎的做法:

  1. 重渲染前,记下当前选中项的行 id / 节点 id。
  2. 重建表格数据。
  3. 按记下的 id 找回那一行,重新选中。

所以插行、换数据都不会选中错位 —— 前提是你给了 row_ids:

// 有 row_ids:选中跟着 r2 走
WidgetTree::table(['名称'], $rows, ['id' => 'tbl', 'row_ids' => ['r0', 'r1', 'r2'], 'current' => 'r2']);

// 没有 row_ids:行 id 退化成索引 '0'、'1'…,插一行后选中会跳到别处
WidgetTree::table(['名称'], $rows, ['id' => 'tbl']);

这就是为什么数据控件反复强调要给 row_ids。

属性级别的 diff

同一个控件内部,也是按属性比对的:只有值真的变了才写回 Qt。

所以 view() 里写:

WidgetTree::label('点击次数:' . $state['clicks'], ['id' => 'count'])

每帧都构造一个新的 label 节点,但 diff 发现 text 没变就什么都不做 —— 不会引起 Qt 重绘。

patch() 为什么是"旁路"

patch() 直接改控件,跳过了树。执行后,diff 引擎记录的"该控件上次的属性签名"就和实际不符了。

所以框架把签名作废 —— 下一次 render() 一律以树为准重新同步。

这解释了 增量补丁 那条核心约束:

追加的行、清空的内容都只在这次渲染之前有效。要长期存在就得写回状态。

代价与收益

声明式 diff命令式句柄
应用代码量只描述"界面该长什么样"要自己维护控件生命周期、增量同步
状态保留自动(靠 id)要手动处理
每帧开销遍历树 + 比对属性只有你显式改的才动
出错方式id 不稳 → 重建(可见的卡)漏同步 → 界面与数据不一致(难查)

框架选了前者:应用代码最省事,代价是每帧一次树遍历。实测这个开销远低于"写命令式同步代码"的心智负担 —— 而且热路径可以用 patch() 单独优化。

相关

  • 架构原理 —— 三层分工与事件旅程
  • 属性 —— 可 diff 的属性全集
  • 增量补丁 —— 绕过 diff 的代价
在 GitHub 上编辑此页
最后更新: 2026/10/6 03:13
Prev
手写桥接
Next
无头验收