编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

  • iOS开发和进阶

  • Web开发和进阶

  • Linux应用开发

    • Linux应用开发
    • QML基础入门

      • QML基础入门
      • 嵌入式GUI技术全景
      • QML引擎与渲染原理
      • QML语法与类型系统
      • 属性绑定与响应式原理
        • 4.1 案例引入
          • 4.1.1 绑定失踪
          • 4.1.2 根因分析
          • 4.1.3 三大问题
        • 4.2 绑定原理
          • 4.2.1 依赖收集
          • 4.2.2 脏标记求值
          • 4.2.3 绑定赋值冲突
          • 4.2.4 依赖收集案例
        • 4.3 依赖图引擎
          • 4.3.1 通知链路
          • 4.3.2 作用域求值
          • 4.3.3 两种绑定方式
          • 4.3.4 帧同步追踪
        • 4.4 绑定恢复
          • 4.4.1 恢复绑定
          • 4.4.2 双向绑定
          • 4.4.3 仪表盘绑定
        • 4.5 性能优化
          • 4.5.1 求值频率
          • 4.5.2 循环检测
          • 4.5.3 Dashboard 优化
        • 4.6 新手陷阱
        • 4.7 训练题
        • 4.8 思考题
        • 4.9 速查表
      • 可视元素与布局原理
      • 事件处理与传播机制
      • 模型视图架构原理
      • 动画与状态机原理
      • Canvas与自定义渲染
      • QML与C++集成原理
      • 自定义SceneGraph节点
      • 交叉编译与部署
      • 嵌入式渲染后端
      • 性能优化与真机调试
    • QT核心库实践

    • Linux系统编程

    • 综合项目实战

  • IoT智能硬件开发

  • Apps
  • Linux应用开发
  • QML基础入门
杨充
2025-06-24
目录

属性绑定与响应式原理

# 第 4 章 属性绑定与响应式原理

本章定位:从"写绑定"到"理解绑定"。第 3 章你学会了在 QML 里写 width: parent.width / 2,本章回答这句声明背后的完整链路:依赖是如何被收集的、变化是如何传播的、为什么 JS 赋值会破坏绑定、以及 Qt.binding() 如何把破坏的绑定恢复。理解这章,你才能在"绑定失效"或"绑定循环"的诡异现象出现时,第一时间定位根因。

# 目录介绍

  • 4.1 案例引入
    • 4.1.1 绑定失踪
    • 4.1.2 根因分析
    • 4.1.3 三大问题
  • 4.2 绑定原理
    • 4.2.1 依赖收集
    • 4.2.2 脏标记求值
    • 4.2.3 绑定赋值冲突
    • 4.2.4 依赖收集案例
  • 4.3 依赖图引擎
    • 4.3.1 通知链路
    • 4.3.2 作用域求值
    • 4.3.3 两种绑定方式
    • 4.3.4 帧同步追踪
  • 4.4 绑定恢复
    • 4.4.1 恢复绑定
    • 4.4.2 双向绑定
    • 4.4.3 仪表盘绑定
  • 4.5 性能优化
    • 4.5.1 求值频率
    • 4.5.2 循环检测
    • 4.5.3 Dashboard 优化
  • 4.6 新手陷阱
  • 4.7 训练题
  • 4.8 思考题
  • 4.9 速查表

# 4.1 案例引入

# 4.1.1 绑定失踪

某 HMI 工程师在车机上写了一段根据车速切换背景色的 QML:

import QtQuick
import QtQuick.Controls

ApplicationWindow {
    width: 800; height: 480; visible: true

    property real speed: 0

    Rectangle {
        id: bg
        anchors.fill: parent
        color: parent.speed > 80 ? "#ff4444" : "#4444ff"   // ① 绑定
    }

    Slider {
        id: slider
        anchors.bottom: parent.bottom
        from: 0; to: 200
        onValueChanged: bg.speed = value                     // ② 更新 speed
    }

    Button {
        anchors.centerIn: parent
        text: "重置"
        onClicked: {
            bg.color = "green"                               // ③ JS 赋值
            // 期望:之后 speed 变化,color 应该在绿、红、蓝之间切换
            // 实际:之后 speed 变化,color 永远是 green,死掉了
        }
    }
}

测试步骤:

1. 拖动 slider 到 90 → 背景变红(speed > 80)                ✅ 绑定生效
2. 点击"重置"按钮                                               ✅ bg.color = "green"
3. 拖动 slider 回到 50 → 背景仍然是 green                     ❌ 绑定死了!
4. 拖动 slider 到 85  → 背景仍然是 green                     ❌ 绑定的确死了

疑惑:

  • 我只是给 bg.color 赋了一次值,为什么绑定就永久失效了?
  • QML 不是号称"响应式"吗?为什么一个赋值就破坏了整个响应式链条?
  • 有没有办法在 JS 赋值后把绑定"救活"?

# 4.1.2 根因分析

bg.color = "green" 是 JavaScript 赋值——它直接把属性设为固定值 "green",同时删除原来挂在 color 上的绑定表达式。

点击前:
  bg.color ← QQmlBinding(表达式: "parent.speed > 80 ? ...")
           ← 依赖 parent.speed
           ← 当 speed 变化 → 重新求值 → 设置新颜色

点击后(bg.color = "green"):
  bg.color ← "green"                    (固定值)
           ← QQmlBinding 已被移除!     (绑定没了)
           ← 不再依赖 parent.speed
           ← speed 变化 → 什么也不发生

核心:QML 的绑定和 JS 赋值共用一个属性槽,赋值时新值覆盖旧值——如果旧值是"绑定表达式",它就被"吃掉了"。

# 4.1.3 三大问题

问题 在哪节回答
写 color: parent.width > 400 ? "red" : "blue" 时,引擎具体做了什么事? §4.2
为什么 JS 赋值 rect.color = "green" 会破坏绑定?能否恢复? §4.3 + §4.4
在嵌入式 60fps 的约束下,绑定求值如何做到不拖累帧率? §4.2.2 + §4.5

读完本章你会亲手写一个带绑定恢复功能的温度仪表盘,并理解 QQmlNotifier 的依赖传播全过程。


# 4.2 绑定原理

# 4.2.1 依赖收集

当你写 color: parent.width > 400 ? "red" : "blue" 时,QQmlEngine 不是"每隔一段时间检查一下 parent.width 变了没"——那样太低效。它用依赖收集让被依赖的属性主动通知:

Step 1: 创建绑定对象
  QQmlBinding 被创建,内部存了:
    - 目标对象 root.rect(id 为 rect 的 Rectangle)
    - 目标属性 "color"
    - 表达式字符串 "parent.width > 400 ? \"red\" : \"blue\""

Step 2: 首次求值(关键:启用依赖收集模式)
  QQmlEngine 设置全局标志 isCollectingDependencies = true
  在 V4 JS 引擎中执行这个表达式:
    parent.width   ⚡ 被读取!QQmlEngine 捕获这次属性访问
    > 400
    ? "red"
    : "blue"
  结果: "blue"(因为 parent.width 初始是 400,不大于 400)
  设置 root.rect->color = "blue"

Step 3: 关闭依赖收集模式,得到依赖列表
  isCollectingDependencies = false
  收集到的依赖: [parent.width]
  QQmlBinding 内部保存: dependencies = [parent.width]

Step 4: 注册通知
  向 parent.width 的 QQmlNotifier 注册:
  "当 parent.width 变化时 → 通知我(rect.color 的绑定)重新求值"

依赖收集的实际执行路径(等价 C++ 伪代码):

// QQmlBinding::evaluate()
QVariant QQmlBinding::evaluate() {
    engine->beginDependencyCollection();    // 第 2 层:挂载全局监听
    
    QV4::Value result = v4->evaluate(m_expression);
    // ↑ V4 执行 JS 表达式时,每个被读取的 QML 属性都会触发:
    //   QObject::property("width") → 检测到 isCollectingDependencies
    //   → 把 "property owner + property name" 加入本轮依赖列表
    
    m_dependencies = engine->endDependencyCollection();  // 第 3 层:拿到依赖列表
    
    // 第 4 层:向每个依赖注册回调
    for (auto& dep : m_dependencies) {
        dep.notifier->addDependent(this);
        // ↑ 当 dep 属性变化 → notifier 遍历所有 dependent → 调用 QQmlBinding::notify()
    }
    
    return result.toQVariant();
}

设计精髓:依赖收集不靠静态分析表达式字符串(那得写一个完整的类型推理引擎),而是让 JIT 执行表达式,边跑边记——这是 V4 引擎与 QQmlEngine 深度协作的经典案例。

# 4.2.2 脏标记求值

当 parent.width 从 400 变成 600 时:

parent.width = 600
  → QQmlNotifier::notify()
  → 遍历 dependents 列表
  → 找到 rect.color 的 QQmlBinding
  → 调用 QQmlBinding::markDirty()       ← 仅设置一个标志位!
  → QQmlBinding::m_dirty = true
  → 如果是在渲染帧之外 → 什么额外操作都不做

下一帧的 Sync 阶段:
  → QQmlEngine::processPendingBindings()
  → 遍历所有 dirty 绑定
  → QQmlBinding::evaluate()
    → parent.width = 600 > 400 → "red"
    → rect->setProperty("color", "red")    ← 这里才真正更新
  → m_dirty = false

为什么不"立刻"求值?看一个极端场景:

如果 parent.width 在一帧内被改了 10 次:
  立刻求值 → rect.color 被设置 10 次 → QQmlNotifier 发出 10 次通知
           → 每次通知都触发 Scene Graph 的 dirtyItem → 浪费 CPU

  惰性求值 → 10 次只标记 dirty = true 10 次(几乎零开销)
           → 渲染帧只求值 1 次 → rect.color 只更新 1 次
           → 只触发 1 次 Scene Graph dirtyItem

惰性求值的三个关键时机——绑定在以下三种情况才会真正求值:

触发条件 例子 开销
渲染帧的 Sync 阶段 每 16.6ms 自动触发 合并多次变化,最经济
属性被读取 (getProperty) console.log(rect.color) 即时求值,仅该属性
属性被写入 (setProperty) rect.color = "blue" 先求值(确认当前值),再覆盖绑定

铁律:绑定表达式的求值成本 = 0(在属性变化但无人读取它的期间)。这就是 QML 能在嵌入式板子上同时运行数百个绑定表达式而不拖垮 CPU 的原因。

# 4.2.3 绑定赋值冲突

现在回到 §4.1 的 Bug。bg.color = "green" 这一行到底做了什么?

bg.color = "green"  ← JavaScript 赋值

底层等价调用:
  QQmlProperty::write(bg, "color", QVariant("green"))
    ↓
  检测到 "color" 属性上当前挂了一个 QQmlBinding(绑定表达式)
    ↓
  QQmlProperty::removeBinding()   ← ① 先卸载绑定
    - 从 parent.speed 的 QQmlNotifier 里注销自己
    - QQmlBinding::destroy() → 释放内存
    ↓
  QQmlProperty::setValue("green")  ← ② 再写入固定值
    ↓
  此后 bg.color = "green"(固定值),再无绑定
  parent.speed 无论怎么变,bg.color 再也不动

对比:绑定求值 vs JS 赋值

操作 代码 底层 绑定存活
声明式绑定 color: parent.speed > 80 ? "red" : "blue" 创建 QQmlBinding → 注册依赖 ✅ 永久(直到被覆盖)
绑定求值 (自动触发) setProperty(color, newValue) → QQmlNotifier 通知 ✅ 绑定仍在
JS 赋值 color = "green" removeBinding() → setProperty("green") ❌ 绑定被杀

关键区分:绑定求值走 QQmlBinding::evaluate() → 内部 setProperty() → QQmlNotifier 通知。JS 赋值走 QQmlProperty::write() → 检测到绑定 → 先杀绑定 → 再写值。两个路径完全不同。

# 4.2.4 依赖收集案例

下面这段代码,用 console.log 把依赖收集的三阶段(①创建绑定、②收集依赖、③注册通知)变成可观察的行为:

import QtQuick
import QtQuick.Controls

ApplicationWindow {
    width: 500; height: 300; visible: true

    property int a: 10
    property int b: 20

    // ───── 绑定 A:依赖 a ─────
    property int resultA: a * 2
    onResultAChanged: console.log("[A] resultA = " + resultA)

    // ───── 绑定 B:依赖 a 和 b ─────
    property int resultB: a + b
    onResultBChanged: console.log("[B] resultB = " + resultB)

    // ───── 非绑定属性(不会被依赖收集) ─────
    property int c: 30

    Component.onCompleted: {
        console.log("=== Step 1: 修改 a → 应触发 A 和 B 的重新求值 ===")
        a = 100
        // 期望:resultA = 200, resultB = 120
        // 控制台打印顺序:
        //   [A] resultA = 200
        //   [B] resultB = 120
        // 注意:先打印谁取决于 QQmlNotifier 的通知顺序(非确定性)

        console.log("=== Step 2: 修改 b → 只应触发 B ===")
        b = 50
        // 期望:resultB = 150, resultA 不变(100 * 2 = 200)
        // 控制台只打印:
        //   [B] resultB = 150

        console.log("=== Step 3: 修改 c → 不应触发任何绑定 ===")
        c = 999
        // 期望:控制台无任何 onChanged 输出
        // 因为没有任何绑定表达式依赖 c
    }
}

控制台典型输出:

=== Step 1: 修改 a → 应触发 A 和 B 的重新求值 ===
[A] resultA = 200
[B] resultB = 120
=== Step 2: 修改 b → 只应触发 B ===
[B] resultB = 150
=== Step 3: 修改 c → 不应触发任何绑定 ===
(无输出)

案例知识融合:本案例验证了三个依赖收集的核心规律——①resultA 依赖 a(a * 2),resultB 依赖 a 和 b(a + b),修改 a 触发两者的 onChanged;②修改 b 只触发 resultB(因为 resultA 不依赖 b);③c 没有任何绑定依赖它,修改 c 不触发任何通知——引擎只在表达式里真正被读取的属性上注册通知。

思考题:

  1. 如果 resultA 的表达式改成 resultA: a * 2 + c * 0(注意 c * 0),修改 c 会触发 resultA 的重新求值吗?为什么引擎不优化掉"乘以 0"这个无用的依赖?
  2. 同时修改 a = 100 和 b = 50(在同一帧内),resultB 会被重新求值一次还是两次?这与惰性求值有什么关系?
  3. c = 999 如果改成 c = a * 3(不是绑定声明,是 JS 赋值),修改 a 后 c 会变吗?这和声明式绑定的区别是什么?

# 4.3 依赖图引擎

# 4.3.1 通知链路

上一节说"当属性变化时,QQmlNotifier 通知所有依赖者"。这个通知链路在 Qt 6 源码里的完整路径:

QObject::setProperty("a", 100)
  ↓
QQuickItem::setProperty("a", 100)         ← 如果属性有 NOTIFY 信号
  ↓
emit aChanged()                            ← C++ 信号发出
  ↓
QQmlNotifier::notify()                     ← 内部监听同名信号
  ↓
遍历 dependents 列表:
  ┌─ QQmlBinding("resultA = a * 2")       ← 某个依赖项
  │    → QQmlBinding::markDirty()
  │    → m_dirty = true
  │
  ├─ QQmlBinding("resultB = a + b")       ← 另一个依赖项
  │    → QQmlBinding::markDirty()
  │    → m_dirty = true
  │
  └─ QQmlBinding("...")                   ← 更多依赖项
       ...
  ↓
QQmlEngine 主循环(事件循环的每次迭代):
  if (hasDirtyBindings()) {
      processPendingBindings()              // 统一求值所有 dirty 绑定
  }

QQmlNotifier 不为"只读属性"自动生成——如果你写的 property int x: 0 没有写 NOTIFY xChanged,它不会自动生成通知器:

// C++ 侧:属性必须带 NOTIFY,QML 绑定才能依赖它
Q_PROPERTY(int speed READ speed WRITE setSpeed NOTIFY speedChanged)
                                            // ↑ 关键:NOTIFY 信号
// QML 侧写 property:引擎自动生成 NOTIFY
property int speed: 0
// 等价于 C++ 里有 signal speedChanged()
// QQmlEngine 为它分配了一个 QQmlNotifier

结论:QML 里的 property 声明 = C++ 里的 Q_PROPERTY(... NOTIFY ...)。引擎为每个带 NOTIFY 的属性自动创建一个 QQmlNotifier——这就是"响应式"的物理载体。

# 4.3.2 作用域求值

绑定的作用域决定了它在哪重求值时能访问哪些变量:

Rectangle {
    id: root
    property int base: 10

    // ① 作用域正确:可以访问 root、parent、base
    width: Math.min(parent.width, root.base * 20)

    // ② 作用域正确:可以访问同级的 child(如果 child 声明在 width 之前)
    // ⚠️ 注意:绑定表达式按声明顺序创建,后声明的属性才能被前声明引用
    height: child.height + 10      // ❌ child 在下面声明——此时还未创建 child 对象
    Rectangle {
        id: child; height: 50
    }

    // ③ 作用域限制:绑定里不能访问 QML 文件中定义的 function 的局部变量
    function fn() { var local = 5; }
    // width: local * 10  ← ❌ local 是 JS 作用域,不在 QML 绑定作用域内
}

绑定声明顺序陷阱:

Item {
    // ❌ 不行——child 还没被创建
    property int h: child.height
    
    Rectangle { id: child; height: 50 }

    // ✅ 可以——inverseChild 在上面声明
    property int h2: inverseChild.height
    
    Rectangle { id: inverseChild; height: 100 }
}

铁律:绑定表达式里引用的 id 必须在表达式之前被声明(即 id 所在的对象在 QML 文件的上面一行)。这个限制来自 QQmlEngine 的单次解析顺序——它从上到下解析、创建、建立绑定,不能"往后引用"。

# 4.3.3 两种绑定方式

前面用的 width: parent.width / 2 叫声明式绑定——写在对象声明的 { } 里。此外还有 Binding {} QML 类型,它可以运行期动态建立/断开绑定:

import QtQuick

Rectangle {
    id: rect
    width: 200; height: 200; color: "blue"

    // 声明式绑定——无法运行时断开
    opacity: slider.value
}

Slider {
    id: slider
    from: 0.0; to: 1.0; value: 1.0
}

对比用 Binding {}:

Rectangle {
    id: rect; width: 200; height: 200; color: "blue"
    // 不写声明式绑定——改为用 Binding 类型
}

Binding {
    id: opacityBinding
    target: rect                       // 目标对象
    property: "opacity"                // 目标属性
    value: slider.value                // 绑定表达式
    when: enableBinding.checked        // 条件控制:enableBinding 勾选时才生效
    delayed: true                      // Qt 6.5+:延迟求值
}

Slider { id: slider; from: 0; to: 1; value: 0.8 }
CheckBox { id: enableBinding; checked: true }
维度 声明式绑定(x: expr) Binding {} 类型
创建时机 QML 加载时一次性 加载时
能否运行时断开 ❌ 不可 ✅ 通过 when: false
能否条件绑定 ❌ 不可 ✅ when: condition
能否跨文件绑定 仅同文件内 ✅ target 可以是任何对象的 id
能否恢复被 JS 赋值破坏的绑定 ❌ 不能 ✅ 可以:Binding { value: Qt.binding(...) }

典型应用场景:Binding {} 最常用于"条件式绑定"——

// 正常模式:slider 驱动 opacity
// 诊断模式:opacity 强制 = 1.0(忽略 slider)
Binding {
    target: rect; property: "opacity"
    value: diagnosticMode ? 1.0 : slider.value
    when: true
}

# 4.3.4 帧同步追踪

下面把"脏标记 → 惰性求值"的过程可视化——每帧打印哪些绑定被重新求值:

import QtQuick
import QtQuick.Controls

ApplicationWindow {
    width: 600; height: 400; visible: true

    property int counter: 0
    property int derived: counter * 2
    property int derived2: counter + derived

    // 监控:每次 derived 改变,记录求值帧号
    property int evalFrame: 0
    onDerivedChanged: {
        console.log("[Frame " + evalFrame + "] derived =", derived,
                    "(counter =", counter + ")")
    }
    onDerived2Changed: {
        console.log("[Frame " + evalFrame + "] derived2 =", derived2,
                    "(counter =", counter + ", derived =", derived + ")")
    }

    // 模拟每帧改一次数据
    Timer {
        interval: 100; repeat: true; running: true
        property int frameId: 0
        onTriggered: {
            frameId++
            evalFrame = frameId
            counter = frameId                     // 每个 timer tick 改一次 counter
            // 期望:每帧 derived 和 derived2 各求值一次
        }
    }

    Rectangle {
        anchors.centerIn: parent
        width: 200; height: 100; radius: 12
        color: derived2 > 10 ? "#4CAF50" : "#FF9800"
        Text {
            anchors.centerIn: parent; font.pixelSize: 18
            text: "counter: " + counter + "\nderived: " + derived
        }
    }
}

控制台输出:

[Frame 1] derived = 2  (counter = 1)
[Frame 1] derived2 = 3 (counter = 1, derived = 2)
[Frame 2] derived = 4  (counter = 2)
[Frame 2] derived2 = 6 (counter = 2, derived = 4)
[Frame 3] derived = 6  (counter = 3)
[Frame 3] derived2 = 9 (counter = 3, derived = 6)
...

观察:虽然 counter 一帧内只改一次,但依赖链 counter → derived → derived2 的两层绑定都在同一帧内完成求值——因为 derived 变化会立即标记 derived2 dirty,而 Sync 阶段遍历所有 dirty 绑定时会一并处理。

案例知识融合:本案例展示了绑定传播链的完整时序——①counter = frameId 触发 counter 的 QQmlNotifier;②通知到达 derived 绑定(标记 dirty)和 derived2 绑定(也直接依赖 counter,标记 dirty);③derived 求值后设置新值 → derived 的 QQmlNotifier 通知 derived2(再次标记 dirty);④derived2 虽被标记两次,但 Sync 阶段只求值一次——这就是"合并脏标记"。

思考题:

  1. 如果把 derived2: counter + derived 改成 derived2: derived * 1(去掉对 counter 的直接依赖),当 counter = 10 时,依赖链是什么?derived2 会被标记几次?
  2. 如果 Timer 的 interval 从 100 改成 5(每 5ms 触发一次),一帧 (16.6ms) 内 counter 会被改 3 次。derived 和 derived2 会被求值几次?和 §4.2.2 的惰性求值结合起来分析。

# 4.4 绑定恢复

# 4.4.1 恢复绑定

回到 §4.1 的 Bug——JS 赋值破坏了绑定。修复方案是 Qt.binding():

Button {
    text: "重置"
    onClicked: {
        // ❌ Bug 写法:赋值破坏绑定
        bg.color = "green"

        // ✅ 修复:用 Qt.binding() 重新建立绑定
        bg.color = Qt.binding(function() {
            return parent.speed > 80 ? "#ff4444" : "#4444ff"
        })
    }
}

Qt.binding() 做了什么?

bg.color = Qt.binding(function() { return parent.speed > 80 ? "…" : "…" })
  ↓
QQmlProperty::write(bg, "color", QVariant)
  ↓ 检测到写入的是一个 QQmlBinding 对象(Qt.binding() 返回 QQmlBinding)
  ↓
① removeBinding()             ← 卸载旧绑(如果有)
② installBinding(newBinding)  ← 安装新绑
  ↓
newBinding 走完整依赖收集流程:
  - 执行 function() → 读取 parent.speed → 记录依赖
  - 向 parent.speed 的 QQmlNotifier 注册
  ↓
此后 parent.speed 变化 → bg.color 重新求值 ✅ 绑定复活

# 4.4.2 双向绑定

Binding {} 类型也能用来建立"复活绑定",而且更适合条件场景:

Rectangle {
    id: rect; width: 200; height: 200; color: "blue"
}

Binding {
    id: rectBinding
    target: rect; property: "color"
    value: controller.colorForValue(slider.value)
    when: useBinding.checked                 // ← 仅在勾选时绑定生效
}

Slider { id: slider; from: 0; to: 100 }
CheckBox { id: useBinding; checked: true }

双向绑定——两个属性互相跟随:

// 声明式双向绑定(两个 Binding 互指)

Slider { id: slider; from: 0; to: 100; value: 50 }
SpinBox { id: spinner; from: 0; to: 100; value: 50 }

Binding {
    target: slider; property: "value"
    value: spinner.value
}

Binding {
    target: spinner; property: "value"
    value: slider.value
}

// slider.value = 70 → spinner.value 变成 70
//                     → 触发 Binding 反向通知 slider.value = 70
//                     → QQmlEngine 检测到值相同(70 → 70)→ 停止传播
// 结论:值相同时自动终止,不会无限循环

双向绑定的安全机制:

slider.value = 70
  → Binding(1) 将 spinner.value 设为 70
    → spinner QQmlNotifier 通知 Binding(2)
      → Binding(2) 检查:slider.value 已经是 70 → 相同 → skip
        → 停止传播 ✅

防循环原理:QQmlEngine 在每次 setProperty 前先比较新旧值——如果值不变,跳过后续的绑定求值和通知。这个"值相等终止"机制让双向绑定成为可能。

# 4.4.3 仪表盘绑定

这个案例把绑定恢复、条件绑定、双向绑定集成到一个车机仪表盘的完整实现:

import QtQuick
import QtQuick.Controls

ApplicationWindow {
    width: 800; height: 480; visible: true; title: "仪表盘绑定系统"

    // ───── 数据层 ─────
    property real speedKMH: 0
    property real rpm: 0
    property real fuelPercent: 100
    property bool maintenanceMode: false

    // ───── 衍生绑定 ─────
    readonly property bool speedWarning: speedKMH > 120
    readonly property bool rpmWarning: rpm > 5000
    readonly property bool anyWarning: speedWarning || rpmWarning

    // ───── 主背景(响应 anyWarning) ─────
    Rectangle {
        id: bg
        anchors.fill: parent
        color: anyWarning ? "#ff4444" : "#222222"

        // 诊断模式下强制绿色
        Binding on color {
            when: maintenanceMode
            value: "#00AA00"
        }

        // ───── 速度显示 ─────
        Text {
            id: speedText
            anchors.centerIn: parent
            font.pixelSize: 72; font.bold: true
            color: speedWarning ? "yellow" : "white"
            text: speedKMH.toFixed(0)
        }

        // ───── 转速条 ─────
        Rectangle {
            id: rpmBar
            anchors { bottom: parent.bottom; horizontalCenter: parent.horizontalCenter }
            width: parent.width * 0.8; height: 20; radius: 10
            color: "#555"
            Rectangle {
                height: parent.height; radius: parent.radius
                width: parent.width * (rpm / 7000)
                color: rpmWarning ? "red" : "#4CAF50"
            }
        }

        // ───── 燃油百分比 ─────
        Text {
            anchors { top: parent.top; right: parent.right; margins: 20 }
            text: "燃料: " + fuelPercent.toFixed(0) + "%"
            color: fuelPercent < 15 ? "red" : "white"
            font.pixelSize: 16
        }
    }

    // ───── 控制面板 ─────
    Row {
        anchors { bottom: parent.bottom; horizontalCenter: parent.horizontalCenter; margins: 30 }
        spacing: 10

        Button {
            text: "重置警报"
            onClicked: {
                // 用 Qt.binding 恢复被模拟数据打断的绑定
                speedText.color = Qt.binding(function() {
                    return speedWarning ? "yellow" : "white"
                })
            }
        }

        Button {
            text: maintenanceMode ? "退出诊断" : "诊断模式"
            onClicked: maintenanceMode = !maintenanceMode
        }
    }

    // ───── 模拟 CAN 总线数据 ─────
    Timer {
        interval: 50; repeat: true; running: true
        onTriggered: {
            speedKMH = 100 + Math.sin(Date.now() / 2000) * 60
            rpm = 3000 + Math.sin(Date.now() / 1500) * 2500
            fuelPercent -= 0.02
            // 仅这三行赋值 → 自动触发 speedWarning/rpmWarning/anyWarning 重算
            // → bg.color 自动切换 → speedText.color 自动切换
        }
    }
}

运行效果:背景根据 anyWarning 自动红黑切换;诊断模式下强制绿色;燃料低于 15% 时文字变红;点击"重置警报"恢复被破坏的绑定。

案例知识融合:本案例集成了本章全部核心概念——①speedWarning 等衍生属性是 QML 声明式绑定,自动依赖收集;②Binding on color { when: maintenanceMode } 是条件绑定,运行时切换生效;③Qt.binding() 在按钮点击时恢复绑定;④所有绑定惰性求值,50ms 的 Timer 多次赋值但每帧只求值一次。


# 4.5 性能优化

# 4.5.1 求值频率

// 陷阱 1:多属性同时变化导致重复求值
property int a: 0
property int b: 0
property int c: 0
property string info: a + " / " + b + " / " + c

// 一帧内同时改 a, b, c:
a = 1; b = 2; c = 3
// → info 被标记 dirty 3 次!但实际只求值 1 次(惰性求值合并)
// → 然而——求值表达式 "a + ' / ' + b + ' / ' + c" 每次都做字符串拼接
// → 如果 info 被 10 个其他属性依赖,且全部在同一帧变更 → 潜在性能问题

优化:对高频变更属性做手动合并

// ✅ 改进:用一个信号处理器做批量更新
property string info: ""

function updateInfo() {
    info = a + " / " + b + " / " + c    // 只求值一次
}

onAChanged: updateInfo()
onBChanged: updateInfo()

// 这样 info 只依赖最后一次赋值——因为前两次赋值已经在 onChanged 里被覆盖

# 4.5.2 循环检测

// ❌ 绑定循环——引擎能检测到但会严重影响性能
Rectangle {
    id: rect
    width: otherRect.width        // A 依赖 B
}
Rectangle {
    id: otherRect
    width: rect.width             // B 依赖 A
}
// → QQmlEngine 检测到循环 → qml: binding loop detected for property "width"
// → 仍然会尝试求值,但限制深度(默认限制 10 层求值深度)

典型绑定循环场景与修复:

场景 错误写法 修复
比例分布 left.width: parent.width - right.width
right.width: parent.width - left.width
left.width: parent.width * 0.5
right.width: parent.width - left.width
颜色跟随 a.color: b.color
b.color: a.color
断开循环:一个用绑定,一个用固定值
Slider-Spinner 同步 互相绑定 用 Binding {} + 值相等自动终止机制(安全)

# 4.5.3 Dashboard 优化

在一个 200 个绑定属性的仪表盘上,用 Qt Quick Profiler 抓到的绑定点耗时:

优化前(反例):

// ❌ 每个 guage 独立计算——200 个绑定表达式
Repeater {
    model: 200
    Item {
        property real gaugeValue: dataModel.values[index]  // × 200
        property color gaugeColor: gaugeValue > threshold ? "red" : "green"  // × 200
        property real gaugeAngle: gaugeValue / maxValue * 270 - 135  // × 200
    }
}
// → Binding 时间线占整帧的 40%(6.6ms)
// → 主要开销:200 个 gaugeColor 各自做字符串比较 > threshold 和颜色赋值

优化 step 1:提取纯度值到 C++

// ColorCalculator.h — C++ 侧,一次计算全 200 个颜色
class ColorCalculator : public QObject {
    Q_OBJECT
public:
    Q_INVOKABLE QColor colorForValue(double value, double threshold) {
        return value > threshold ? QColor("red") : QColor("green");
    }
};
// QML 侧:颜色计算走 C++
ColorCalculator { id: calc }

Repeater {
    model: 200
    Item {
        property color gaugeColor: calc.colorForValue(dataModel.values[index], threshold)
        // ↑ C++ 执行,不走 V4 JS 绑定路径
    }
}
// → Binding 时间线降至 1.2ms(↓ 82%)

优化 step 2:提取重复求值的绑定到中间属性

// ❌ 200 个 gaugeAngle 各自算一遍 270 - 135
// ✅ 把常量提取:
readonly property real angleRange: 270
readonly property real angleOffset: -135

Repeater {
    model: 200
    Item {
        property real gaugeAngle: gaugeValue / maxValue * angleRange + angleOffset
    }
}

优化前后对比:

版本 Binding 求值时间/帧 占帧比 措施
v0 反例 6.6ms 40% 无
v1 颜色走 C++ 1.2ms 7% Q_INVOKABLE 替代 JS 颜色比较
v2 + 常量提取 0.8ms 5% 提取重复常量为 readonly property

案例知识融合:本案例验证了三个绑定性能铁律——①能用 C++ 做的计算不要留在 QML 绑定里(V4 JS 路径比 C++ 慢 5-20 倍);②重复计算的常量提取为 readonly property,避免每次绑定求值都重算;③绑定表达式越简单,惰性求值的合并窗口越大。


# 4.6 新手陷阱

# 陷阱 说明 修复
1 JS 赋值破坏绑定 rect.color = "green" 不是"临时覆盖",是"永久杀死绑定" 用 Qt.binding() 恢复;或用 Binding {} 控制何时生效
2 绑定里引用"后面声明的 id" width: child.width 但 child 在后面声明 → 运行时 undefined id 引用必须在绑定表达式的前面声明
3 修改 model 数据不走绑定 ListModel 的 set() 触发数据更新,但不触发依赖收集 用 Context property 连接 model 和绑定
4 绑定循环静默性能恶化 控制台无 warning,但 Profiler 显示 Binding 时间飙升 用 Qt Quick Profiler 抓 Binding 时间线;检查"互相依赖"的对
5 Binding { when: false } 期间直接赋值不生效 when: false 时 QML 认为该属性"被 Binding 管理"——外部赋值被忽略 仅在绑定生效时才用 when;否则用 Qt.binding 手动控制

# 4.7 训练题

训练题 1:诊断绑定被吃掉的 Bug

要求:下面的 QML 有 2 处"绑定被吃掉"的 Bug。找出并修复。

import QtQuick
import QtQuick.Controls

ApplicationWindow {
    width: 400; height: 300; visible: true
    property int value: 50

    Rectangle {
        id: bar
        width: parent.value * 3
        height: 30; color: "steelblue"
    }

    Button {
        text: "click"
        onClicked: {
            bar.width = 100              // ← Bug 1
            if (value > 80) {
                bar.color = "red"        // ← Bug 2(注意 bar.color 是声明式绑定吗?)
            }
        }
    }

    Slider {
        anchors.bottom: parent.bottom
        from: 0; to: 100
        onValueChanged: parent.value = value
    }
}
修复方案
onClicked: {
    // Bug 1 修复:用 Qt.binding 恢复绑定
    bar.width = Qt.binding(function() { return parent.value * 3 })

    // Bug 2: bar.color 没有声明式绑定——color 在 Rectangle 里没写绑定
    // 所以 bar.color = "red" 只是赋值,不算 Bug。
    // 真正的陷阱是:因为你没写 color 绑定,所以你不会意识到它已经被赋值覆盖了。
    // 如果之前有 color: parent.value > 80 ? "red" : "blue" 这个声明,
    // 那 color = "red" 就破坏了它——需要同样用 Qt.binding 恢复。
}

训练题 2:写一个带绑定恢复功能的实时时钟

要求:用 Timer 每秒更新一次当前时间,Text 显示时分秒。Button 点击暂停更新(冻结显示),再次点击恢复更新。必须用 Qt.binding() 实现恢复。

// Clock.qml — 参考答案
import QtQuick
import QtQuick.Controls

ApplicationWindow {
    width: 300; height: 200; visible: true

    property string currentTime: ""
    property bool paused: false

    Text {
        id: clock
        anchors.centerIn: parent
        text: currentTime
        font.pixelSize: 48
    }

    Timer {
        interval: 1000; repeat: true; running: !paused
        onTriggered: {
            currentTime = new Date().toLocaleTimeString()
        }
    }

    Button {
        anchors { bottom: parent.bottom; horizontalCenter: parent.horizontalCenter }
        text: paused ? "恢复" : "暂停"
        onClicked: {
            paused = !paused
            if (!paused) {
                // 恢复绑定——时钟文字跟随 currentTime 更新
                clock.text = Qt.binding(function() { return currentTime })
            }
        }
    }
}

# 4.8 思考题

  1. 绑定 vs Observer 模式:QML 的绑定机制本质上是"属性之间的 Observer 模式"。对比 RxJS / Vue 的响应式系统,QML 的惰性求值 + 脏标记与它们的"push / pull"模型各有什么优劣?为什么嵌入式要选择惰性求值?

  2. 绑定表达式为什么不用 WebAssembly 加速:V4 的 JIT 已经可以把 JS 编译为 ARM 机器码,为什么不在 QML 绑定表达式中引入更激进的编译优化——比如编译期展开 parent.width * 0.8 为 C++ 代码?这个决定的取舍是什么?(提示:想想 CompilationUnit 的 objectTable 和 bindingTable 的设计)

  3. 依赖循环的工程意义:QQmlEngine 检测到绑定循环时只是打印 warning 而不 throw。但有些场景"循环"是期望行为(如双向同步的两个 Slider)。请举出至少一个"有意义的绑定循环"场景,并解释为什么引擎仍然警告、但设计上允许。


# 4.9 速查表

概念 一句话
QQmlBinding 绑定对象:目标对象 + 目标属性 + JS 表达式 + 依赖列表
依赖收集 首次求值时,V4 执行 JS 表达式,QQmlEngine 边跑边记"读了哪些属性"
QQmlNotifier 每个带 NOTIFY 的属性的通知器——属性变化时遍历 dependents 通知
脏标记 (dirty flag) 绑定被标记 dirty = true——"需要重新求值",但还没求
惰性求值 标记 dirty 后,仅在渲染 Sync 阶段或属性被读取时才真正求值
JS 赋值 = 杀绑定 rect.color = "green" → removeBinding() + setProperty()
Qt.binding() 在 JS 中恢复被破坏的绑定——传入 function() 重建 QQmlBinding
Binding {} 类型 QML 声明式绑定对象:target + property + value + when,运行期可控制
双向绑定 两个 Binding 互指对方属性——值相同时引擎自动停止传播
绑定循环 A.width: B.width 且 B.width: A.width——引擎检测并告警

核心哲学:

QML 的"响应式"不是魔法——
它是 QQmlNotifier 的通知链路 + 依赖收集 + 惰性求值协作的结果。

理解绑定的"创建 → 收集 → 脏标记 → 惰性求值"四段循环,
就等于掌握了 QML 响应式系统的全部秘密。

下一篇:05. 可视元素与布局原理 (opens new window)

上次更新: 2026/07/12, 19:39:51
QML语法与类型系统
可视元素与布局原理

← QML语法与类型系统 可视元素与布局原理→

最近更新
01
audit
07-27
02
C++入门教程全章思考题汇编
07-24
03
12.技术团队建设能力
07-21
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式