QML

Привязки свойств (property bindings): механизм и оптимизация

Введение

Статья 479 кратко показала пример привязки (width: container.width * 0.5) — в этой статье разберём механизм привязок подробнее: как движок QML отслеживает зависимости и определяет, когда нужно пересчитать значение, и какие практические соображения важны для эффективного использования привязок.

Концепция

Привязка свойства — выражение JavaScript (статья 483), записанное как значение свойства, которое движок QML автоматически переоценивает каждый раз, когда любое из значений, от которых это выражение зависит, изменяется — движок строит граф зависимостей автоматически, анализируя, какие свойства каких объектов упоминаются в выражении привязки, и подписывается на сигналы изменения именно этих свойств. Это принципиально отличается от простого, разового присваивания значения — присваивание (width = 100) выполняется один раз и не пересматривается позже, тогда как привязка (width: someOtherItem.width) продолжает действовать постоянно, обновляя width при каждом изменении someOtherItem.width, пока сама эта привязка не будет явно заменена другим присваиванием или привязкой.

Пример кода: Привязки свойств (property bindings)

import QtQuick

Rectangle {
    id: root
    width: 400; height: 200

    property int itemCount: 5 // обычное, "хранимое" свойство

    Rectangle {
        id: dynamicBox
        // Привязка — автоматически пересчитывается при изменении ЛЮБОГО из участвующих свойств
        width: root.width / root.itemCount
        height: root.height
        color: "coral"
    }

    // Изменение itemCount автоматически пересчитает dynamicBox.width БЕЗ какого-либо явного кода для этого
    Slider {
        from: 1; to: 10
        onValueChanged: root.itemCount = Math.round(value)
    }
}
// Демонстрация разницы между привязкой и разовым присваиванием
Rectangle {
    id: root
    property int baseSize: 100

    Rectangle {
        id: boundBox
        width: root.baseSize * 2 // ПРИВЯЗКА — будет обновляться при изменении root.baseSize
    }

    Rectangle {
        id: assignedBox
        Component.onCompleted: {
            width = root.baseSize * 2 // РАЗОВОЕ присваивание в JavaScript — больше НЕ обновится
                                        // при последующем изменении root.baseSize!
        }
    }
}
// Разрыв привязки явным присваиванием — частый источник путаницы
Rectangle {
    id: box
    width: parent.width / 2 // изначально это ПРИВЯЗКА

    MouseArea {
        anchors.fill: parent
        onClicked: box.width = 200
        // ^ это присваивание ПОЛНОСТЬЮ заменяет привязку — после клика box.width больше
        // НЕ будет реагировать на изменения parent.width, оставаясь зафиксированным на 200
    }
}

Пояснения к коду

dynamicBox.width: root.width / root.itemCount — привязка, зависящая сразу от двух свойств (root.width и root.itemCount) — движок QML отслеживает оба, и изменение любого из них (в том числе через Slider.onValueChanged, изменяющий itemCount) автоматически вызывает пересчёт dynamicBox.width без необходимости писать явный код синхронизации. Сравнение boundBox (объявленная привязка) и assignedBox (присваивание внутри Component.onCompleted) демонстрирует ключевое практическое различие — несмотря на одинаковое первоначальное вычисленное значение, только boundBox продолжит реагировать на будущие изменения root.baseSize, тогда как assignedBox.width навсегда останется тем значением, что было вычислено в момент выполнения присваивания. Третий пример показывает частую, неочевидную для новичков ловушку — присваивание значения свойству, ранее имевшему привязку, не «добавляет» дополнительное условие к существующей привязке, а полностью её заменяет и отключает, что для разработчика, ожидающего сохранения исходного «динамического» поведения, может стать источником путаницы.

Подводные камни

  • Случайный разрыв привязки явным императивным присваиванием значения тому же свойству, как показано в примере с box.width = 200 — это одна из самых частых, неочевидных для новичков ошибок в QML, и разработчик, ожидающий, что свойство продолжит реагировать на изменения, обнаруживает (часто не сразу), что оно «застряло» на последнем явно присвоенном значении; явное понимание этого поведения — необходимая часть владения QML.
  • Создание циклических зависимостей привязок (свойство A зависит от свойства B, которое зависит от свойства A) — это может привести либо к ошибке движка QML при обнаружении явного цикла, либо к неопределённому, труднопредсказуемому поведению (бесконечный цикл пересчёта или использование «замороженного» промежуточного значения), и привязки должны образовывать направленный, ацикличный граф зависимостей.
  • Чрезмерно сложные, дорогие в вычислении выражения привязок, пересчитываемые при каждом изменении часто меняющегося зависимого свойства (например, привязка, зависящая от позиции мыши, обновляемой много раз в секунду при движении) — каждый пересчёт привязки имеет свою вычислительную стоимость, и для производительности (статья 494) важно следить, чтобы выражения привязок, зависящие от часто изменяющихся свойств, оставались дешёвыми в вычислении, перенося более дорогую логику в JavaScript-функции, вызываемые реже или асинхронно.
  • Предположение, что привязки автоматически «оптимизируют» неэффективный, избыточно сложный код, освобождая разработчика от необходимости думать о производительности — хотя механизм привязок сам по себе эффективен (пересчёт происходит только при реальном изменении зависимостей, а не постоянно), сложность и стоимость самого выражения привязки целиком остаётся на ответственности разработчика, и привязки не являются «бесплатной» заменой продуманной, эффективной логики там, где такая логика реально нужна.