Работа с Сигналами
Пока что мы использовали простые примеры с create_signal, возвращающие геттер ReadSignal и сеттер WriteSignal.
Чтение и запись данных
Есть четыре простые операции с сигналами:
.get()клонирует текущее значение сигнала и реактивно отслеживает будущие изменения..with()принимает функцию, получающую текущее значеие сигнала по ссылке (&T) и отслеживает будущие изменения..set()заменяет текущее значение сигнала и уведомляет об этом подписчиков..update()принимает функцию, которая принимает мутабельную ссылку на текущее значение сигнала (&mut T) и уведомляет подписчиков. (.update()не возвращает значение возвращённое замыканием; для этого можно использовать.try_update(), если, например, при удалении элемента изVec<_>нужно его вернуть.)
Вызов ReadSignal как функции это синтаксический сахар для.get(). Вызов WriteSignalкак функции это синтаксический сахар для .set().
Так что
let (count, set_count) = create_signal(0);
set_count(1);
logging::log!(count());
это то же самое, что
let (count, set_count) = create_signal(0);
set_count.set(1);
logging::log!(count.get());
Можно заметить, что и .get() и .set() могут быть реализованы при помощи .with() и .update().
Другими словами, count.get() идентично count.with(|n| n.clone()), а count.set(1) реализуется посредством count.update(|n| *n = 1).
Но конечно же, .get() и .set() (или простые вызовы функций!) это намного более приятный синтаксис.
Однако, и для .with() с .update() есть очень хорошее применение.
Например, возьмём сигнала с типом Vec<String>.
let (names, set_names) = create_signal(Vec::new());
if names().is_empty() {
set_names(vec!["Alice".to_string()]);
}
С точки зрения логики, этот пример достаточно прост, но в нём скрываются существенные недостатки, влияющие на производительности.
Помните, что names().is_empty() это сахар для names.get().is_empty(), который клонирует значение (it’s names.with(|n| n.clone()).is_empty()).
Это значит мы клонируем значение Vec<String> целиком, выполняем is_empty(), и тут же выбрасываем клонированное значение.
Аналогично и set_names заменяет значение новым Vec<_>. В общем-то ничего страшного, но мы можем просто мутировать исходный Vec<_> там где он лежит.
let (names, set_names) = create_signal(Vec::new());
if names.with(|names| names.is_empty()) {
set_names.update(|names| names.push("Alice".to_string()));
}
Теперь наша функция просто принимает names по ссылке и запускает is_empty(), избегая клонирования.
Пользователи Clippy и те, у кого острый глаз, могли заметить, что можно сделать ещё лучше:
if names.with(Vec::is_empty) {
// ...
}
В конце концов, .with() просто принимает функцию, которая принимает значение по ссылке.
Поскольку Vec::is_empty принимает &self, мы можем передать её напрямую и избавиться от ненужного замыкания.
Есть вспомогательные макросы, упрощающие использование .with() и .update(), особенно при работе с несколькими сигналами сразу.
let (first, _) = create_signal("Bob".to_string());
let (middle, _) = create_signal("J.".to_string());
let (last, _) = create_signal("Smith".to_string());
Если хочется конкатенировать эти 3 сигнала вместе без ненужного клонирования, пришлось бы написать что-то вроде:
let name = move || {
first.with(|first| {
middle.with(|middle| last.with(|last| format!("{first} {middle} {last}")))
})
};
Очень длинно и писать неприятно.
Вместо этого можно использовать макрос with!, чтобы получить ссылки на все эти сигналы сразу.
let name = move || with!(|first, middle, last| format!("{first} {middle} {last}"));
Это превращается в то, что было выше. Посмотрите документацию к with! для дополнительной информации,
и макросам update!, with_value! и update_value!.
Делаем так, чтобы сигналы зависели друг от друга
Люди часто спрашивают о ситуациях, когда какой-то сигнал должен меняться в зависимости от значения другого сигнала. Для этого есть три хороших способа и один неидеальный, но сносный в контролируемых обстоятельства.
Хорошие варианты
1) Б это функция от А. Создадим сигнал для А и производный сигнал или memo для Б.
let (count, set_count) = create_signal(1);
let derived_signal_double_count = move || count() * 2;
let memoized_double_count = create_memo(move |_| count() * 2);
Рекомендации о том как выбирать между производным сигналом и memo можно найти в документации к
create_memo
2) В это функция от А и Б. Создадим сигналы для А и Б, а также производный сигнал или memo для В.
let (first_name, set_first_name) = create_signal("Bridget".to_string());
let (last_name, set_last_name) = create_signal("Jones".to_string());
let full_name = move || with!(|first_name, last_name| format!("{first_name} {last_name}"));
3) А и Б — независимые сигналы, но иногда они обновляются одновременно.. Когда вы обновляете A, отдельно вызываете и обновление Б.
let (age, set_age) = create_signal(32);
let (favorite_number, set_favorite_number) = create_signal(42);
// use this to handle a click on a `Clear` button
let clear_handler = move |_| {
set_age(0);
set_favorite_number(0);
};
Если без этого никак...
4) Создайте эффект, чтобы писать в Б каждый раз когда А меняется. Так делать официально не рекомендуется по нескольким причинам:
a) Это всегда будет более затратно, так как это значит, что каждый раз, когда А обновляется, будет два полных прохода через реактивный процесс.
(устанавливается значение А, это вызывает запуск этого эффекта, наряду с остальными, зависящими от А. Затем меняется Б, что вызовет выполнение всех зависящих от Б эффектов.)
b) Это повышает вероятность нечаянно сделать бесконечный цикл или эффекты, выполняющиеся слишком часто.
Это тот самый пинг-понг, происходивший в реактивном спагетти-коде в начале 2010-х годов, которого мы стараемся избежать
посредством разделения чтения и записи, а также не рекомендуя писать в сигналы из эффектов.
В большинстве ситуаций лучше переписать всё таким образом, чтобы был чёткий поток данных сверху вниз, основанный на производных сигналах или мемоизированных значениях. Но это не конец света.
Я намеренно не привожу здесь пример кода. Прочтите документацию к
create_effectпонять как это должно работать.