Примечание: Реактивность и Функции

Один из наших контрибьютеров в ядро сказал мне недавно: — Я никогда не использовал замыкания так часто, пока не начал использовать Leptos. И это правда. Замыкания это сердце любого Leptos приложения.
Иногда это выглядит немного глупо:

// a signal holds a value, and can be updated
let (count, set_count) = create_signal(0);

// a derived signal is a function that accesses other signals
let double_count = move || count() * 2;
let count_is_odd = move || count() & 1 == 1;
let text = move || if count_is_odd() {
    "odd"
} else {
    "even"
};

// an effect automatically tracks the signals it depends on
// and reruns when they change
create_effect(move |_| {
    logging::log!("text = {}", text());
});

view! {
    <p>{move || text().to_uppercase()}</p>
}

Замыкания, кругом замыкания!

Но почему?

Функции и UI фреймворки

Функции это сердце любого UI фреймворка. И немудрено. Создание пользовательского интерфейса по сути можно разбить на две части:

  1. первоначальный рендеринг
  2. обновления

При использовании Веб-фреймворка, именно фреймворк какой-то первоначальный рендеринг. Затем он передаёт контроль обратно браузеру. Когда срабатывают определенные события (такие, как нажатие мыши) или завершаются асинхронные задачи (например, завершается HTTP запрос), браузер будит фреймворк, чтобы обновить что-то. Фреймворк выполняет какой-то код, чтобы обновить интерфейс пользователя, и снова засыпает до тех пор, пока браузер не разбудит его снова.

Ключевой фразой здесь является "выполняет какой-то код". Естественный способ "вызвать какой-то код" в произвольный момент (в Rust или в любом другом языке программирования) это вызвать функцию. И фактически каждый UI фреймворк основан на повторном выполнении некоего рода функции снова и снова:

  1. фреймворки с виртуальным DOM (VDOM) такие, как React, Yew или Dioxus перезапускают компонент или рендерную функцию снова и снова чтобы сгенерировать виртуальное дерево DOM, которое может быть сверено с предыдущим результатом, чтобы внести правки в DOM
  2. компилирующие фреймворки как Angular и Svelte разделяют шаблоны компонента на функции "создать" и "обновить", повторно выполняя функцию "обновить" всякий раз, когда они обнаруживают, что состояние компонента изменилось
  3. во фреймворках с мелкозернистой реактивностью, таких как SolidJS, Sycamore, или Leptos, вы задаёте функции, которые выполняются повторно.

Это то, что все наши компоненты делают.

Возьмём привычный нам пример <SimpleCounter/> в его простейшей форме:

#[component]
pub fn SimpleCounter() -> impl IntoView {
    let (value, set_value) = create_signal(0);

    let increment = move |_| set_value.update(|value| *value += 1);

    view! {
        <button on:click=increment>
            {value}
        </button>
    }
}

Сама по себе функция SimpleCounter выполняется единожды. Сигнал value создается один раз. Фреймворк передаёт функцию increment браузеру в качестве слушателя событий. При нажатии на кнопку, браузер вызывает increment, которая обновляет value через set_value. И это обновляет единственный текстовый узел представленный в нашем view выражением {value}.

Замыкания это ключ к реактивности. Они дают возможность фреймворку повторно выполнить самый маленький элемент в приложении в ответ на изменение.

Так что помните две вещи:

  1. Функция компонента это установочная функция, не рендерная функция: она выполняется лишь раз.
  2. Чтобы значения в шаблоне view были реактивными, они должны быть функциями: либо сигналами (они реализуют типажи Fn) или замыканиями.