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
    });
}

问题本质:

  1. ‌JavaScript 的变量作用域‌:var 声明的变量是函数作用域,而非块级作用域。
  2. ‌闭包共享变量‌:所有 Qt.binding 回调函数共享同一个 index 变量。
  3. ‌异步执行‌:绑定函数在稍后执行时,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 绑定的特殊注意事项

  1. ‌动态绑定‌:Qt.binding 会创建一个动态属性绑定,当依赖的属性(如 QMLAdapter16ms["TTPosition" + index].source)变化时,表达式会重新计算。
  2. ‌作用域隔离‌:闭包中的变量需要确保与外部作用域隔离,否则可能产生意外的共享状态。
  3. ‌性能‌:避免在频繁触发的代码(如动画)中创建过多闭包绑定。

  

posted @ 2025-03-17 17:17  蓦然而然  阅读(53)  评论(0)    收藏  举报