QML中的闭包绑定
有一个业务场景.
在C++中有大量类似的Q_PROPERTY,它们都是由相同字符串+数字构成。例如ABC+1,ABC+2
在QMl中也有对应的Image来加载它们的图片。Image的命名为也是字符串+数字,例如abc+1,abc+2
在启动时需要读取配置文件,在配置文件中的abc+number将会显示一次,之后需要重新绑定ABC+number的值。
示例代码:
1 Item { 2 id: main 3 property bool isCheck: false 4 property var childrenMap: [] 5 property var checks: [] 6 Component.onCompleted: { 7 //NOTE Stores all child objects of telltales 8 for (var i = 0; i < children.length; ++i) { 9 childrenMap[children[i].objectName] = children[i] 10 } 11 checks = Adapter.getChecks() //QVarintMap (position1,source1)(position2,source2)... 12 } 13 14 Image { 15 id: position1 16 objectName: "position1" 17 width: 56 18 height: 56 19 anchors { 20 left: parent.left 21 leftMargin: l1 22 top: parent.top 23 topMargin: t1 24 } 25 visible: true 26 source: (Adapter.Position1.source !== "") ? "icons/" + Adapter.Position1.source : "" 27 opacity: isCheck ? blink : Adapter.Position1.state 28 } 29 // 省略中间的代码2-99 30 //position 100 31 Image { 32 id: position100 33 objectName: "position100" 34 width: 56 35 height: 56 36 anchors { 37 left: parent.left 38 leftMargin: l100 39 top: parent.top 40 topMargin: t100 41 } 42 visible: true 43 source: (Adapter.Position100.source !== "") ? "icons/" + Adapter.Position100.source : "" 44 opacity: isCheck ? blink : Adapter.Position100.state 45 } 46 47 Connections { 48 target: Adapter 49 function onCheckStarted() { 50 isCheck = true 51 for (let key in checks) { 52 let child = childrenMap[key] 53 if (child) { 54 child.source = "icons/" + checks[key] 55 child.blink = true 56 } 57 } 58 } 59 function onCheckEnd() { 60 for (let key in checks) { 61 let child = childrenMap[key] 62 if (child) { 63 child.source = "" 64 let index = key.slice(8) 65 child.source = Qt.binding(function () { 66 return (Adapter["Position" + index].source !== "") ? "icons/" + Adapter["Position" + index].source : "" 67 }) 68 // child.source = Qt.binding((function (idx) { 69 // return function () { 70 // return (Adapter["Position" + idx].source !== "") ? "icons/" + Adapter["Position" + idx].source : "" 71 // } 72 // })(key.slice(8))) 73 child.blink = true 74 } 75 } 76 isBulbCheck = false 77 } 78 } 79 }
记录原因:
1. 可以使用Adapter["Position" + index].source的写法来访问C++的属性,以前没有这样使用过,并且这个属性是一个自定义类。
2. 由于出现了闭包绑定问题:
什么是闭包?
闭包是 函数及其创建时所在词法作用域的组合。简单来说:
- 函数可以记住并访问它被创建时的作用域中的变量,即使该函数在原始作用域外执行。
- 闭包会捕获变量的引用,而不是变量的值。
代码中的闭包问题
for 循环中:
for (var key in checks) { var index = key.slice(8); // ... child.source = Qt.binding(function() { // 这里引用了 index }); }
问题本质:
- JavaScript 的变量作用域:
var声明的变量是函数作用域,而非块级作用域。 - 闭包共享变量:所有
Qt.binding回调函数共享同一个index变量。 - 异步执行:绑定函数在稍后执行时,
index的值已经是循环结束后的最终值。
结果:
- 所有绑定函数最终都会使用最后一次循环的
index值,而不是每次迭代时的值。
解决方案
要让闭包捕获每次循环的 index 值,需为每个闭包创建独立的变量副本。以下是两种常见方法:
方法 1:使用 let 声明块级作用域变量(ES6+)
1 for (let key in checks) { 2 let child = childrenMap[key] 3 if (child) { 4 child.source = "" 5 let index = key.slice(8) 6 child.source = Qt.binding(function () { 7 return (Adapter["Position" + index].source !== "") ? "icons/" + Adapter["Position" + index].source : "" 8 }) 9 child.blink = true 10 } 11 }
原理:
let为每次循环创建一个新的块级作用域。- 每个闭包捕获的是当前循环的
index,互不干扰。
方法 2:通过立即调用函数表达式(IIFE)创建闭包
1 child.source = Qt.binding((function (idx) { 2 return function () { 3 return (Adapter["Position" + idx].source !== "") ? "icons/" + Adapter["Position" + idx].source : "" 4 } 5 })(key.slice(8)))
原理:
- 通过 IIFE 立即执行一个函数,将
index作为参数idx传入。 - 每次循环都会创建一个新的函数作用域,
idx是独立的副本。
QML 绑定的特殊注意事项
- 动态绑定:
Qt.binding会创建一个动态属性绑定,当依赖的属性(如QMLAdapter16ms["TTPosition" + index].source)变化时,表达式会重新计算。 - 作用域隔离:闭包中的变量需要确保与外部作用域隔离,否则可能产生意外的共享状态。
- 性能:避免在频繁触发的代码(如动画)中创建过多闭包绑定。
浙公网安备 33010602011771号