原文 ?QML Engine Internals, Part 2: Bindings
譯者注:這個解析QML引擎的文章共4篇畜普,分析非常透徹魁兼,在國內(nèi)幾乎沒有找到類似的分析,為了便于國內(nèi)的QT/QML愛好者和工作者也能更好的學習和理解QML引擎,故將這個系列的4篇文章翻譯過來咐汞。翻譯并不是完全直譯,有不足之處儒鹿,請指正化撕,謝謝!
———————————————————————————————————————————
上一篇:QML文件加載? ? ?下一篇 綁定類型
該博文是深入解析QML引擎系列博文中的第二篇约炎。在上一篇博文中植阴,我們已經(jīng)為大家揭示了QML引擎是如何加載QML文件的。簡明扼要地回顧一下:解析QML文件圾浅,并為文件中的所有元素創(chuàng)建C ++對象掠手。例如,QML文件中包含一個Text(文本)元素狸捕,QML引擎就會創(chuàng)建一個C ++ ?QQuickText類的實例喷鸽。
QML引擎主要用于處理QML文件的加載,加載之后的運行時階段就不再那么需要它了灸拍。運行時的事件處理和繪制等都是由它生成的C++類來完成的做祝。例如,TextInput(文本輸入)元素的輸入事件是由QQuickTextInput::keyPressEven()處理鸡岗,繪制則由QQuickTextInput::updatePaintNode()實現(xiàn)混槐,完全不需要QML引擎參與。
但是在運行時轩性,QML引擎依然會涉及到兩個重要的東西:信號處理器和屬性綁定更新(譯者注:屬性綁定更新声登,其實就是計算屬性右邊表達式的值,后續(xù)有詳細的講解揣苏,這個地方不用擔心不明白悯嗓。)。比如MouseArea的一個onClicked處理器就是信號處理器(譯者注:MouseArea是第一篇博文的例子中的一個元素舒岸,從第一篇分析的內(nèi)容绅作,我們了解onClicked這種這樣的信號處理器也被看作是屬性值,和普通的屬性沒啥差別)蛾派。我們將在這篇文章中深入分析綁定(bindings)俄认。在此之前請先看下面這個例子:
在這個例子中,包含了給屬性賦值的兩種方式:
1. 簡單的賦一個值洪乍,比如給QQuickRectangle的width屬性賦值300眯杏。對應的VME指令是STORE_DOUBL。它會在組件創(chuàng)建后執(zhí)行壳澳,只是簡單的調(diào)用函數(shù)QMetaObject::metacall(QMetaObject::WriteProperty,…), ?該函數(shù)最終執(zhí)行QQuickRectangle:setWidth()來設置width屬性的值岂贩。在初始化之后,QML引擎再也不會修改width屬性的值了巷波。
2. 賦一個綁定(binding)萎津,比如給text屬性賦一個綁定 "Window Area:" +(parent.width* parent.height)卸伞,給anchors.centerIn屬性賦一個綁定 parent。綁定的神奇之處在于锉屈,當Rectangle的height和width屬性改變時荤傲,會自動更新到text屬性。這是如何實現(xiàn)的呢颈渊?其實也沒那么神奇杈帐,接下來我們將為你揭曉它的運作機制她君。(譯者注:原作者說的binding到底是什么,下面馬上就會揭曉,不用擔心投储。)
創(chuàng)建綁定
通過設置QML_COMPILER_DUMP=1來輸出VME指令晌区,我們可以看到例子中的兩個綁定都是由指令STORE_COMPILED_BINDING創(chuàng)建的:
編譯后綁定是一種優(yōu)化的綁定睹簇,我們還是先研究一下普通綁定驶悟,它是由STORE_BINDING指令創(chuàng)建的。查看QQmlVME::run()的代碼妹田,我們發(fā)現(xiàn)代碼中創(chuàng)建了一個QQmlBinding對象唬党,并把 "function $text() { return "Window Area:"+ (parent.width *parent.height) }" 做為它的表達式。沒錯鬼佣,每一個綁定都是一個JavaScript函數(shù)驶拱!"function $text()" 這部分代碼是由QML編譯器添加的,這是因為QML的JavaScript引擎V8只支持完整的函數(shù)晶衷。這個JavaScript函數(shù)緊接著會被V8編譯器編譯成一個V8::Function對象蓝纲。因為V8引擎有一個實時(JIT)編譯器,所以它會生成本地的機器碼(譯者注:傳統(tǒng)的JavaScript引擎是把JavaScript代碼先編譯為字節(jié)碼晌纫,然后再通過解釋器執(zhí)行字節(jié)碼税迷,V8引擎運用JIT技術(shù),不通過解釋器執(zhí)行字節(jié)碼锹漱,而是直接把JavaScript代碼編譯成運行在CPU(x86/x64/ARM)上的機器碼)箭养。這時,V8:: Function對象并不會被執(zhí)行哥牍,但是它會一直保留毕泌。
STORE_BINDING指令創(chuàng)建一個綁定可總結(jié)為:先創(chuàng)建了一個QQmlBinding對象,然后該對象借助V8引擎把傳給它的JavaScript函數(shù)編譯成了一個V8::Function對象嗅辣。
(譯者注:為了更容易理解后續(xù)的內(nèi)容撼泛,在這里約定“綁定”即JavaScript函數(shù),“綁定對象”即QQmlBinding對象澡谭,計算綁定的值即表示運行JavaScript函數(shù)求值愿题,或者執(zhí)行V8::Function代碼求值)
運行綁定
在某些時候,綁定需要被運行,這意味著讓V8引擎對綁定求值并將結(jié)果賦值給對應的屬性潘酗。這些都是在創(chuàng)建階段的最后階段完成的杆兵。
QQmlVME::complete()會調(diào)用每個綁定對象的update()函數(shù),在我們的例子中就是QQmlBinding:: update()函數(shù)仔夺。update()只是簡單的執(zhí)行v8:Function對象并將返回值賦給目標屬性,這在我們的例子中就是Rectangle的text屬性拧咳。
但是V8引擎是怎么知道parent.width和parent.height的值的呢?說實在的囚灼,它究竟是怎么知道parent對象的?答案就是:它不知道祭衩。V8引擎沒有任何線索知道到底存在哪些對象灶体,類名是什么,也不知道這些對象的屬性是什么掐暮。當V8引擎遇到一個未知類或未知屬性時蝎抽,它會詢問QML引擎中的一個對象包裹器(Object Wrapper),這個對象包裹器會找到正確的類或?qū)傩月房耍阉麄兎祷亟oV8引擎樟结。下面我們通過堆棧信息來看一看QQuickItem的width屬性是如何被訪問的:
從上面的堆棧信息來看,我們發(fā)現(xiàn)qv8qobjectwrapper.cpp中的包裹類最終調(diào)用函數(shù)QObject::qt_metacall(QMetaObject::ReadProperty,…) 來獲取屬性值精算。首先包裹類被V8代碼調(diào)用瓢宦,然后V8代碼又被V8::Function對象對應的機器碼調(diào)用。由于機器碼沒有堆棧幀(stack frames)灰羽,因此GDB(調(diào)試工具)沒法顯示在??之后的堆棧信息驮履。在上面的堆棧信息中我做了一點點假,其實它是由兩個獨立的堆棧信息拼起來的廉嚼,細心的讀者會發(fā)現(xiàn)玫镐,堆棧幀的序號并是不連續(xù)的。
由上可知怠噪,V8引擎會使用一個對象包裹類來獲取屬性值恐似。同理,它會使用一個上下文包裹類來找到對象傍念。例如矫夷,在我們的例子中,計算綁定值的過程中需要訪問parent對象捂寿,就是通過這種方式來找到parent的口四。
綜上所述:通過運行編譯后的V8::Function代碼來對綁定進行求值,再由V8引擎通過Qt里的包裹類來訪問對象和屬性秦陋,然后將求的值賦給目標屬性蔓彩。
更新綁定
好了,現(xiàn)在我們知道text屬性是如何獲得它的初始值的。但是綁定更新是如何實現(xiàn)的赤嚼?當height和width屬性改變時旷赖,QML引擎是怎么知道需要重新對綁定求值的呢?
這個問題的答案就隱藏在對象包裹類中更卒。你應該還記得等孵,當V8引擎需要訪問一個屬性時,就會調(diào)用它蹂空。這個對象包裹類不止返回屬性值:它還會捕獲所有被訪問過的屬性俯萌。從根本上講,當一個屬性被訪問時上枕,對象包裹類會調(diào)用綁定對象的捕獲函數(shù)咐熙,在我們的例子中就是QQmlJavaScriptExpression::GuardCapture::captureProperty() ?(QQmlBinding是QQmlJavaScriptExpression的子類)。在捕獲函數(shù)內(nèi)部實現(xiàn)中辨萍,只是簡單地把綁定對象的一個槽函數(shù)連接到被捕獲屬性的NOTIFY信號棋恼。當NOTIFY信號被觸發(fā)時,與之連接的槽函數(shù)就會被調(diào)用锈玉,并重新計算綁定的值爪飘。如果你還沒有聽說過NOTIFY信號拉背,也不用擔心,這很簡單:當一個屬性用Q_PROPERTY來聲明時抡诞,在那里就可能聲明了一個NOTIFY信號昼汗。只要屬性發(fā)生改變鬼雀,擁有該屬性的對象就會觸發(fā)NOTIFY信號源哩。比如,QQuickItem的width屬性的聲明類似如下:
Q_PROPERTY(qrealwidth READ width WRITE setWidth NOTIFY widthChanged)
在我們這個例子中谓着,首次運行綁定赊锚,訪問width屬性時舷蒲,該屬性的捕獲函數(shù)將綁定對象中的一個槽函數(shù)連接到widthChanged()信號。在此之后堤框,只要QQuickItem觸發(fā)widthChanged()信號纵柿,對應的槽函數(shù)將被調(diào)用,并重新計算綁定的值资昧。
這就是為什么當你的屬性發(fā)生改變時荆忍,擁有并觸發(fā)NOTIFY信號是非常的重要撤缴。假如你忘了這樣做屈呕,綁定的值就不會被重新計算,基本上蟋软,屬性綁定就無法正確的運作嗽桩。另一方面碌冶,盡管屬性并沒有真正地改變扑庞,但你也觸發(fā)了NOTIFY信號罐氨,那么綁定的值也會被毫無意義地重新計算栅隐。
綜上所述:當訪問屬性時,對象包裹類會調(diào)用綁定對象的捕捉函數(shù)邑遏,它會將綁定對象的一個槽函數(shù)連接到該屬性的NOTIFY信號记盒,以便當屬性改變時重新計算綁定的值憎蛤。
結(jié)論
在這篇博文中,我們已經(jīng)深入分析綁定是如何工作的纪吮。總結(jié)成一句簡短的話就是:每個綁定都是一個編譯過的JavaScript函數(shù)碾盟,當任何一個引用的屬性改變時,它將重新被計算冰肴。
我希望你喜歡閱讀這些屈藐,我確信深入研究綁定的本質(zhì)是非常有趣的熙尉。
在這個系列的下一篇博文中联逻,我們將了解不同的綁定類型。現(xiàn)在检痰,我們只研究了最基本的綁定,QQmlBinding铅歼,但我們知道還存在更多的綁定類型公壤,比如編譯后綁定厦幅。它們神秘的面紗即將被揭開,敬請關(guān)注缚态!
如果有什么疑問或者對QML應用和研究感興趣的朋友,歡迎加入我們進行討論(QQ群:280689979)玫芦。如需轉(zhuǎn)載桥帆,無須我們授權(quán)医增,但需要注明原文鏈接(該文的鏈接),及原作者老虫,謝謝叶骨!
上一篇:QML文件加載? ? ?下一篇 綁定類型