原文 QML Engine Internals, Part 2: Bindingshtml
译者注:这个解析QML引擎的文章共4篇,分析很是透彻,在国内几乎没有找到相似的分析,为了便于国内的QT/QML爱好者和工做者也能更好的学习和理解QML引擎,故将这个系列的4篇文章翻译过来。翻译并非彻底直译,有不足之处,请指正,谢谢!app
———————————————————————————————————————————函数
该博文是深刻解析QML引擎系列博文中的第二篇。在上一篇博文中,咱们已经为你们揭示了QML引擎是如何加载QML文件的。简明扼要地回顾一下:解析QML文件,并为文件中的全部元素建立C ++对象。例如,QML文件中包含一个Text(文本)元素,QML引擎就会建立一个C ++ QQuickText类的实例。学习
QML引擎主要用于处理QML文件的加载,加载以后的运行时阶段就再也不那么须要它了。运行时的事件处理和绘制等都是由它生成的C++类来完成的。例如,TextInput(文本输入)元素的输入事件是由QQuickTextInput::keyPressEven()处理,绘制则由QQuickTextInput::updatePaintNode()实现,彻底不须要QML引擎参与。优化
可是在运行时,QML引擎依然会涉及到两个重要的东西:信号处理器和属性绑定更新(译者注:属性绑定更新,其实就是计算属性右边表达式的值,后续有详细的讲解,这个地方不用担忧不明白。)。好比MouseArea的一个onClicked处理器就是信号处理器(译者注:MouseArea是第一篇博文的例子中的一个元素,从第一篇分析的内容,咱们了解onClicked这种这样的信号处理器也被看做是属性值,和普通的属性没啥差异)。咱们将在这篇文章中深刻分析绑定(bindings)。在此以前请先看下面这个例子:ui
在这个例子中,包含了给属性赋值的两种方式:翻译
1. 简单的赋一个值,好比给QQuickRectangle的width属性赋值300。对应的VME指令是STORE_DOUBL。它会在组件建立后执行,只是简单的调用函数QMetaObject::metacall(QMetaObject::WriteProperty,…), 该函数最终执行QQuickRectangle:setWidth()来设置width属性的值。在初始化以后,QML引擎不再会修改width属性的值了。调试
2. 赋一个绑定(binding),好比给text属性赋一个绑定 "Window Area:" +(parent.width* parent.height),给anchors.centerIn属性赋一个绑定 parent。绑定的神奇之处在于,当Rectangle的height和width属性改变时,会自动更新到text属性。这是如何实现的呢?其实也没那么神奇,接下来咱们将为你揭晓它的运做机制。(译者注:原做者说的binding究竟是什么,下面立刻就会揭晓,不用担忧。)orm
经过设置QML_COMPILER_DUMP=1来输出VME指令,咱们能够看到例子中的两个绑定都是由指令STORE_COMPILED_BINDING建立的:
编译后绑定是一种优化的绑定,咱们仍是先研究一下普通绑定,它是由STORE_BINDING指令建立的。查看QQmlVME::run()的代码,咱们发现代码中建立了一个QQmlBinding对象,并把 "function $text() { return "Window Area:"+ (parent.width *parent.height) }" 作为它的表达式。没错,每个绑定都是一个JavaScript函数!"function $text()" 这部分代码是由QML编译器添加的,这是由于QML的JavaScript引擎V8只支持完整的函数。这个JavaScript函数紧接着会被V8编译器编译成一个V8::Function对象。由于V8引擎有一个实时(JIT)编译器,因此它会生成本地的机器码(译者注:传统的JavaScript引擎是把JavaScript代码先编译为字节码,而后再经过解释器执行字节码,V8引擎运用JIT技术,不经过解释器执行字节码,而是直接把JavaScript代码编译成运行在CPU(x86/x64/ARM)上的机器码)。这时,V8:: Function对象并不会被执行,可是它会一直保留。
STORE_BINDING指令建立一个绑定可总结为:先建立了一个QQmlBinding对象,而后该对象借助V8引擎把传给它的JavaScript函数编译成了一个V8::Function对象。
(译者注:为了更容易理解后续的内容,在这里约定“绑定”即JavaScript函数,“绑定对象”即QQmlBinding对象,计算绑定的值即表示运行JavaScript函数求值,或者执行V8::Function代码求值)
在某些时候,绑定须要被运行,这意味着让V8引擎对绑定求值并将结果赋值给对应的属性。这些都是在建立阶段的最后阶段完成的。
QQmlVME::complete()会调用每一个绑定对象的update()函数,在咱们的例子中就是QQmlBinding:: update()函数。update()只是简单的执行v8:Function对象并将返回值赋给目标属性,这在咱们的例子中就是Rectangle的text属性。
可是V8引擎是怎么知道parent.width和parent.height的值的呢?说实在的,它到底是怎么知道parent对象的?答案就是:它不知道。V8引擎没有任何线索知道到底存在哪些对象,类名是什么,也不知道这些对象的属性是什么。当V8引擎遇到一个未知类或未知属性时,它会询问QML引擎中的一个对象包裹器(Object Wrapper),这个对象包裹器会找到正确的类或属性,并把他们返回给V8引擎。下面咱们经过堆栈信息来看一看QQuickItem的width属性是如何被访问的:
从上面的堆栈信息来看,咱们发现qv8qobjectwrapper.cpp中的包裹类最终调用函数QObject::qt_metacall(QMetaObject::ReadProperty,…) 来获取属性值。首先包裹类被V8代码调用,而后V8代码又被V8::Function对象对应的机器码调用。因为机器码没有堆栈帧(stack frames),所以GDB(调试工具)无法显示在??以后的堆栈信息。在上面的堆栈信息中我作了一点点假,其实它是由两个独立的堆栈信息拼起来的,细心的读者会发现,堆栈帧的序号并是不连续的。
由上可知,V8引擎会使用一个对象包裹类来获取属性值。同理,它会使用一个上下文包裹类来找到对象。例如,在咱们的例子中,计算绑定值的过程当中须要访问parent对象,就是经过这种方式来找到parent的。
综上所述:经过运行编译后的V8::Function代码来对绑定进行求值,再由V8引擎经过Qt里的包裹类来访问对象和属性,而后将求的值赋给目标属性。
好了,如今咱们知道text属性是如何得到它的初始值的。可是绑定更新是如何实现的?当height和width属性改变时,QML引擎是怎么知道须要从新对绑定求值的呢?
这个问题的答案就隐藏在对象包裹类中。你应该还记得,当V8引擎须要访问一个属性时,就会调用它。这个对象包裹类不止返回属性值:它还会捕获全部被访问过的属性。从根本上讲,当一个属性被访问时,对象包裹类会调用绑定对象的捕获函数,在咱们的例子中就是QQmlJavaScriptExpression::GuardCapture::captureProperty() (QQmlBinding是QQmlJavaScriptExpression的子类)。在捕获函数内部实现中,只是简单地把绑定对象的一个槽函数链接到被捕获属性的NOTIFY信号。当NOTIFY信号被触发时,与之链接的槽函数就会被调用,并从新计算绑定的值。若是你尚未据说过NOTIFY信号,也不用担忧,这很简单:当一个属性用Q_PROPERTY来声明时,在那里就可能声明了一个NOTIFY信号。只要属性发生改变,拥有该属性的对象就会触发NOTIFY信号。好比,QQuickItem的width属性的声明相似以下:
Q_PROPERTY(qrealwidth READ width WRITE setWidth NOTIFY widthChanged)
在咱们这个例子中,首次运行绑定,访问width属性时,该属性的捕获函数将绑定对象中的一个槽函数链接到widthChanged()信号。在此以后,只要QQuickItem触发widthChanged()信号,对应的槽函数将被调用,并从新计算绑定的值。
这就是为何当你的属性发生改变时,拥有并触发NOTIFY信号是很是的重要。假如你忘了这样作,绑定的值就不会被从新计算,基本上,属性绑定就没法正确的运做。另外一方面,尽管属性并无真正地改变,但你也触发了NOTIFY信号,那么绑定的值也会被毫无心义地从新计算。
综上所述:当访问属性时,对象包裹类会调用绑定对象的捕捉函数,它会将绑定对象的一个槽函数链接到该属性的NOTIFY信号,以便当属性改变时从新计算绑定的值。
在这篇博文中,咱们已经深刻分析绑定是如何工做的。总结成一句简短的话就是:每一个绑定都是一个编译过的JavaScript函数,当任何一个引用的属性改变时,它将从新被计算。
我但愿你喜欢阅读这些,我确信深刻研究绑定的本质是很是有趣的。
在这个系列的下一篇博文中,咱们将了解不一样的绑定类型。如今,咱们只研究了最基本的绑定,QQmlBinding,但咱们知道还存在更多的绑定类型,好比编译后绑定。它们神秘的面纱即将被揭开,敬请关注!