属性绑定与响应式原理
# 第 4 章 属性绑定与响应式原理
本章定位:从"写绑定"到"理解绑定"。第 3 章你学会了在 QML 里写
width: parent.width / 2,本章回答这句声明背后的完整链路:依赖是如何被收集的、变化是如何传播的、为什么 JS 赋值会破坏绑定、以及 Qt.binding() 如何把破坏的绑定恢复。理解这章,你才能在"绑定失效"或"绑定循环"的诡异现象出现时,第一时间定位根因。
# 目录介绍
# 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 不触发任何通知——引擎只在表达式里真正被读取的属性上注册通知。
思考题:
- 如果
resultA的表达式改成resultA: a * 2 + c * 0(注意c * 0),修改c会触发resultA的重新求值吗?为什么引擎不优化掉"乘以 0"这个无用的依赖? - 同时修改
a = 100和b = 50(在同一帧内),resultB会被重新求值一次还是两次?这与惰性求值有什么关系? 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 阶段只求值一次——这就是"合并脏标记"。
思考题:
- 如果把
derived2: counter + derived改成derived2: derived * 1(去掉对counter的直接依赖),当counter = 10时,依赖链是什么?derived2会被标记几次? - 如果
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.widthright.width: parent.width - left.width | left.width: parent.width * 0.5right.width: parent.width - left.width |
| 颜色跟随 | a.color: b.colorb.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 思考题
绑定 vs Observer 模式:QML 的绑定机制本质上是"属性之间的 Observer 模式"。对比 RxJS / Vue 的响应式系统,QML 的惰性求值 + 脏标记与它们的"push / pull"模型各有什么优劣?为什么嵌入式要选择惰性求值?
绑定表达式为什么不用 WebAssembly 加速:V4 的 JIT 已经可以把 JS 编译为 ARM 机器码,为什么不在 QML 绑定表达式中引入更激进的编译优化——比如编译期展开
parent.width * 0.8为 C++ 代码?这个决定的取舍是什么?(提示:想想 CompilationUnit 的 objectTable 和 bindingTable 的设计)依赖循环的工程意义: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 响应式系统的全部秘密。