В Dexie.js реактивность строится вокруг liveQuery,
возвращающего наблюдаемый поток, который автоматически пересчитывает
результат при изменении данных в IndexedDB. Подписка на такой поток
формирует объект, управляющий жизненным циклом вычисления, кеширования и
повторного выполнения запроса.
Ключевая особенность модели заключается в том, что каждый подписанный поток существует до явного завершения или до уничтожения внешнего контекста, в котором он был создан. Это делает управление отпиской критически важным для предотвращения утечек памяти и лишних вычислений.
Функция Dexie.liveQuery() формирует ленивый
observable-поток. Вычисление запроса не происходит до момента подписки.
При подписке запускается выполнение функции, возвращающей Promise,
который преобразуется в поток значений.
import { liveQuery } fr om "dexie";
const observable = liveQuery(async () => {
return await db.todos.wh ere("done").equals(0).toArray();
});
Под капотом создаётся вычислительный контекст, привязанный к текущей транзакции чтения IndexedDB. При любом изменении данных, затрагивающих индексы запроса, происходит инвалидирование результата и повторное выполнение функции.
Подписка создаётся через метод subscribe, возвращающий
объект управления жизненным циклом:
const subscription = observable.subscribe({
next: value => console.log(value),
error: err => console.error(err)
});
Объект подписки содержит как минимум:
unsubscribe() — завершение потокаПосле вызова unsubscribe() дальнейшие эмиссии значений
прекращаются, а все внутренние ресурсы освобождаются.
Отписка в Dexie.js не ограничивается остановкой внешних уведомлений. Она разрывает всю цепочку реактивного пересчёта:
Особенность заключается в том, что повторный вызов
unsubscribe() является безопасным и не вызывает ошибок, что
делает жизненный цикл устойчивым к гонкам состояния.
Внутренне liveQuery использует механизм отмены через
AbortController. Каждый запуск вычисления получает сигнал
отмены, позволяющий прерывать запрос при:
const observable = liveQuery(async (signal) => {
const data = await db.items.toArray({ signal });
return data;
});
Передача сигнала позволяет IndexedDB-операциям быть прерванными до завершения, снижая нагрузку при частых изменениях данных.
Каждое изменение данных, затрагивающее используемые таблицы или индексы, приводит к инвалидированию текущего результата. Dexie не мутирует существующее значение, а инициирует полный повторный расчёт.
С точки зрения жизненного цикла подписки это означает:
Это предотвращает промежуточные неконсистентные состояния.
Основной риск связан с удержанием активных подписок при длительном существовании приложения. Поскольку каждая подписка удерживает ссылки на:
неосвобождённые подписки формируют цепочки удержания памяти.
Типичные сценарии:
subscriptionliveQuery без завершения предыдущего
потокаКаждый вызов subscribe() создаёт независимый поток
выполнения. Даже при идентичном запросе liveQuery не
гарантирует шаринг результата между подписчиками.
const obs = liveQuery(() => db.items.toArray());
const sub1 = obs.subscribe(v => console.log("A", v));
const sub2 = obs.subscribe(v => console.log("B", v));
Обе подписки получают независимые уведомления, хотя логически используют одну и ту же функцию запроса.
Это означает, что оптимизация через повторное использование observable-объекта не эквивалентна объединению потоков.
В UI-архитектурах подписки часто привязываются к жизненному циклу компонентов. При смене контекста выполнение должно завершаться до создания нового потока.
Сценарий гонки:
Без корректной отписки происходит дублирование вычислений и лишние запросы к IndexedDB.
Если функция liveQuery выбрасывает исключение, поток
переходит в состояние ошибки. В этом состоянии:
const obs = liveQuery(async () => {
throw new Error("fail");
});
Обработчик error получает управление, но поток не
восстанавливается без пересоздания подписки.
Метод unsubscribe() проектируется как идемпотентный.
Повторные вызовы не изменяют состояние и не приводят к исключениям.
Это важно в условиях неопределённого жизненного цикла, когда:
При завершении подписки происходит очистка:
Особое внимание уделяется разрыву связи между наблюдателем и транзакцией чтения, так как IndexedDB транзакции живут ограниченное время и могут удерживать блокировки.
Подписки Dexie формируют каскадную структуру:
Отписка на верхнем уровне инициирует обратное каскадное завершение всех зависимых узлов графа. Это предотвращает утечки даже при сложных цепочках реактивных вычислений.
При частом пересоздании observable возникает паттерн “подписка-дестрой”:
В таких условиях стоимость управления жизненным циклом становится сопоставимой с самим запросом, поэтому корректная отписка влияет на общую производительность приложения не меньше, чем оптимизация индексов.
В браузерных средах жизненный цикл подписки часто пересекается с:
Dexie не управляет этим автоматически, поэтому все активные подписки должны завершаться внешним контролем, иначе транзакции могут оставаться в подвешенном состоянии до завершения процесса выполнения среды.