Этот пост является продолжением данной (в переводе) статьи.
В предыдущем посте мы рассмотрели, как движок QML загружает файлы. Напомню, что сначала QML-файлы разбираются парсером, затем компилируются в промежуточный байт-код и наконец следуя инструкция байт-кода, для каждого эемента в каждом QML-файле создаётся C++ объект. Например, мы видели, что когда QML файл содержит элемент Text, то движок создает экземпляр C++ класса QQuickText.
На самом деле, загрузка файлов — это почти всё, чем занимается движок QML. После загрузки, он перестаёт вмешиваться в процесс работы приложения. Обработка событий и отрисовка элементов в Runtime полностью ложится на плечи C++. Например, элемент TextInput обрабатывает события вроде QQuickTextInput::keyPressEvent() и обновления QQuickTextInput::updatePaintNode(), без участия движка QML.
Но существуют две важные вещи, на которые движок QML всё же влияет во время выполнения: Связанные обработчики сигналов (Bound signal handlers) и обновление привязок свойств (property binding). Связанные обработчики сигналов — это такие вещи, как onClicked обработчик для MouseArea. Сегодня же мы рассмотрим привязки (Bindings).
Пример:
import QtQuick 2.0
Rectangle {
width: 300
height: 300
color: "lightsteelblue"
Text {
anchors.centerIn: parent
text: "Window Area: " + (parent.width * parent.height)
}
}
Данный пример содержит два вида присвоения свойства:
- Простое присвоение свойств, это например присвоению значения 300 свойству width элемента Rectangle, относящегося к C++ классу QQuickRectangle.В данном случае VME просто выполняет байткод-инструкцию STORE_DOUBLE, во время создания данного компонента. VME просто вызывает функцию QMetaObject :: metacall (QMetaObject :: WriteProperty, ...) которая в итоге заканчивается вызовом QQuickRectangle :: SetWidth (). После этого присвоения. движок QML просто забывает об этом присвоении.
- Присвоение привязки, такое как в примере выше «Window Area: » + (parent.width * parent.height) свойству text или parent свойству centerIn. Благодаря магии привязок, свойство text будет автоматически обновляться при любом изменении высоты или ширины элемента Rectangle. Как это работает? На самом деле никакого волшебства, читайте дальше, чтобы найти ответ.
Создание привязки
Если мы выставим параметр QML_COMPILER_DUMP=1, то мы увидим, что биндинги создаются инструкцией STORE_COMPILED_BINDING:
...
9 STORE_COMPILED_BINDING 43 1 0
10 FETCH 19
11 STORE_COMPILED_BINDING 17 0 1
...
Скомпилированные привязки (Compiled bindings), это на самом деле оптимизированный вариант. Давайте для начаа взглянем на нормальные привязки, которые создаются инструкцией STORE_BINDING. Функция QQmlVME::run() создаёт объект QQmlBinding, которому передаёт строку вида
function $text() { return “Window Area: ” + (parent.width * parent.height) }
являющуюся JS-выражением.
Именно так, каждая привязка является JavaScript-функцией. Часть функции “function $text()” добавляется компилятором QML, таким образом так как V8, являющийся JS-движком QML, может, как несложно догадаться, выполнять только JS-код. Строковая функция затем компилируется в объект v8::Function, компилятором v8. Движок v8, за счёт встроенного JIT-компилятора, создаёт нативный код. Он не выполняет созданный объект v8::Function, но сохраняет его на будущее.
Итого: При выполнении инструкции STORE_BINDING, создаётся объект QQmlBinding, который компилирует переданную ему в виде строки функцию в объект v8::Function, используя для этого встроенный JS-движок V8.
Выполнение привязки
В какой-то момент, нашу привязку будет необходимо выполнить, а это значит что V8 должен будет выполнить переданную ему функцию с телом привязки и вернуть результат выполнения для присвоения его целевому свойству. Данный вызов происходит в первую очередь в конце фазы создания, функция QQmlVME::complete() последовательно вызывает метод update() для каждой привязки. В нашем случае, вызывается функция QQmlBinding::update(). Метод update() просто выполняет содержимое объекта v8::Function и записывает результат выполнения в свойство text нашего прямоугольника.
Но погодите-ка, а как V8 узнаёт о значении переменных parent.width и parent.height? Действительно, откуда он узнаёт о родительском объекте? Ответ: V8 понятия не имеет ни о каких объектах типа QObject, представленных в QML файле, как и о составе их свойств. Когда V8 встречается с неизвестным ему объектом или неизвестным свойством какого-либо объекта, он задаёт вопрос wrapper -объекту движка QML, который находит подходящий объект или свойство и передаёт его (или его значение) обратно в V8. Давайте посмотрим, как происходит доступ к свойству width, объекта QQuickItem на примере данного дампа:
#0 QQuickItem::width (this=0x6d8580) at items/qquickitem.cpp:4711
#1 0x00007ffff78e592d in QQuickItem::qt_metacall (this=0x6d8580, _c=QMetaObject::ReadProperty, _id=8, _a=0x7fffffffc270) at .moc/debug-shared/moc_qquickitem.cpp:675
#2 0x00007ffff7a61689 in QQuickRectangle::qt_metacall (this=0x6d8580, _c=QMetaObject::ReadProperty, _id=9, _a=0x7fffffffc270) at .moc/debug-shared/moc_qquickrectangle_p.cpp:526
#3 0x00007ffff7406dc3 in ReadAccessor::Direct (object=0x6d8580, property=..., output=0x7fffffffc2c8, n=0x0) at qml/v8/qv8qobjectwrapper.cpp:243
#4 0x00007ffff7406330 in GenericValueGetter (info=...) at qml/v8/qv8qobjectwrapper.cpp:296
#5 0x00007ffff49bf16a in v8::internal::JSObject::GetPropertyWithCallback (this=0x363c64f4ccb1, receiver=0x363c64f4ccb1, structure=0x1311a45651a9, name=0x3c3c6811b7f9) at ../3rdparty/v8/src/objects.cc:198
#6 0x00007ffff49c11c3 in v8::internal::Object::GetProperty (this=0x363c64f4ccb1, receiver=0x363c64f4ccb1, result=0x7fffffffc570, name=0x3c3c6811b7f9, attributes=0x7fffffffc5e8)
at ../3rdparty/v8/src/objects.cc:627
#7 0x00007ffff495c0f1 in v8::internal::LoadIC::Load (this=0x7fffffffc660, state=v8::internal::UNINITIALIZED, object=..., name=...) at ../3rdparty/v8/src/ic.cc:933
#8 0x00007ffff4960ff5 in v8::internal::LoadIC_Miss (args=..., isolate=0x603070) at ../3rdparty/v8/src/ic.cc:2001
#9 0x000034b88ae0618e in ?? ()
...
[more ?? frames from the JIT'ed v8::Function code]
...
#1 0x00007ffff481c3ef in v8::Function::Call (this=0x694fe0, recv=..., argc=0, argv=0x0) at ../3rdparty/v8/src/api.cc:3709
#2 0x00007ffff7379afd in QQmlJavaScriptExpression::evaluate (this=0x6d7430, context=0x6d8440, function=..., isUndefined=0x7fffffffcd23) at qml/qqmljavascriptexpression.cpp:171
#3 0x00007ffff72b7b85 in QQmlBinding::update (this=0x6d7410, flags=...) at qml/qqmlbinding.cpp:285
#4 0x00007ffff72b8237 in QQmlBinding::setEnabled (this=0x6d7410, e=true, flags=...) at qml/qqmlbinding.cpp:389
#5 0x00007ffff72b8173 in QQmlBinding::setEnabled (This=0x6d7448, e=true, f=...) at qml/qqmlbinding.cpp:370
#6 0x00007ffff72c15fb in QQmlAbstractBinding::setEnabled (this=0x6d7448, e=true, f=...) a /../../qtbase/include/QtQml/5.0.0/QtQml/private/../../../../../../qtdeclarative/src/qml/qml/qqmlabstractbinding_p.h:98
#7 0x00007ffff72dcb14 in QQmlVME::complete (this=0x698930, interrupt=...) at qml/qqmlvme.cpp:1292
#8 0x00007ffff72c72ae in QQmlComponentPrivate::complete (enginePriv=0x650560, state=0x698930) at qml/qqmlcomponent.cpp:919
#9 0x00007ffff72c739b in QQmlComponentPrivate::completeCreate (this=0x698890) at qml/qqmlcomponent.cpp:954
#10 0x00007ffff72c734c in QQmlComponent::completeCreate (this=0x698750) at qml/qqmlcomponent.cpp:947
#11 0x00007ffff72c6b2f in QQmlComponent::create (this=0x698750, context=0x68ea30) at qml/qqmlcomponent.cpp:781
#12 0x00007ffff79d4dce in QQuickView::continueExecute (this=0x7fffffffd2f0) at items/qquickview.cpp:445
#13 0x00007ffff79d3fca in QQuickViewPrivate::execute (this=0x64dc10) at items/qquickview.cpp:106
#14 0x00007ffff79d4400 in QQuickView::setSource (this=0x7fffffffd2f0 at items/qquickview.cpp:243
#15 0x0000000000400d70 in main ()
Мы видим, что wrapper в файле qv8qobjectwrapper.cpp вызывает функцию QObject::qt_metacall(QMetaObject::ReadProperty, …), для получения значения требуемого свойства. Wrapper (Объект-обёртка) был вызван из скомпилированного V8 кода, хранящегося в V8::Function. Сгенерированный машинный код к сожалению не имеет стека вызовов, поэтому GDB не в состоянии показать что творится за ??. Я вас немного обманул и показал выше два разных стека вызовов, что немного объясняет разрыв в нумерации строк.
Ещё раз: V8 использует объект-обёртку (object wrapper) для получения значений свойств. Таким же образом он использует обёртку-контекст (context wrapper), позволяющий искать сами объекты. например — родительский объект к которому мы обращаемся во время выполнения привязки.
Итак, привязка выполняется запуском кода V8::Function. Движок V8 получает доступ к неизвестным объектам и свойствам вызовом обёрток из Qt. Результат выполнения V8::Function записывается в целевое свойство.
Обновление привязки
Хорошо, мы теперь знаем как свойство text получило своё первоначальное значение. А как насчёт его обновления? Откуда движок QML узнаёт о том, что нужно обновить данное свойство при изменении высоты или ширины родительского прямоугольника?
Ответ на данный вопрос живёт в в том же самом объекте-обёртке (object wrapper), который, как Вы помните вызывается когда V8 требуется получить доступ к свойству. Наша обёртка делает немного больше, чем просто возвращает значение свойства: она записывает все свойства, к которым был запрошен доступ. По существу, когда происходит доступ к свойству, обёртка вызывает функцию захвата привязки, работающей в данный момент. В нашем примере, это QQmlJavaScriptExpression::GuardCapture::captureProperty() (QQmlBinding — это подкласс класса QQmlJavaScriptExpression).
В функции захвата, привязка просто напросто присоединяется в сигналу типа NOTIFY свойства, которое было запрошено. Теперь, когда будет вызван NOTIFY-сигнал, будет вызван подключенный к нему слот привязки и сама привязка будет перезапущена. Если вы никогда не слышали о NOTIFY-сигнале, не волнуйтесь, его логика проста: Когда свойство определено с использованием макроса Q_PROPERTY, то в этом месте всегда будет присутсвовать сигнал NOTIFY, который будет высылаться всегда, когда будет изменяться данное свойство.
Для примера, вот декларация свойства width в классе QQuickItem:
Q_PROPERTY(qreal width READ width WRITE setWidth NOTIFY widthChanged)
В нашем сценарии: когда к свойству width обратятся впервые, во время первого запуска нашей привязки, функция захвата свойства, соединит сигнал widthChanged() и слот запуска нашей привязки. Теперь, когда во время работы приложения объект QQuickItem эмитирует сигнал widthChanged(), все присоединённые к нему привязки будут вызваны и перезапущены.
Очень важно иметь NOTIFY-сигнал в самостоятельно определяемых свойствах и высылать его всегда, когда изменяется наше свойство. Если Вы забудете сделать это, то привязка не будет перезапущена и соответственно, привязка свойства не будет работать корректно. С другой стороны, если NOTIFY-сигнал будет выслан в момент, когда свойство не было изменено, привязка сработает вхолостую.
Подведём итог: Когда происходит доступ к свойству, объект-обёртка вызывает функцию захвата из привязки, которая присоединяет эту привязку к NOTIFY-сигналу свойства, чтобы при изменении свойства привязка выполнялась заново…
Заключение
Сегодня мы рассмотрели то, как работают привязки в QML. Кратко: привязка — это JS-функция, которая выполняется каждый раз когда изменяются задействованные в ней свойства.
Я надеюсь Вам была интересна эта статья, мне например было очень интересно изучить внутреннюю логику работы привязок.
В следующей своей заметке мы рассмотрим различные типы привязок. Всё что мы рассмотрели сегодня — это базовая привязка, или QQmlBinding, но мы знаем, что существуют и другие типы, например скопилированные привязки. Их тайну мы разгадаем в ближайшее время, следите за обновлениями!
Автор: vitaly_KF