Владельцы и очистка
Владелец (owner) нужен модели, которая создается во время выполнения и должна позже отвязать работу, которую создала.
Это часто нужно модальным окнам, чатам, вкладкам документов, медиаплеерам, таймерам и подпискам на браузерные API. Риск не в самом значении стора. Риск в работе вокруг него: реакции, интервалы, внешние слушатели, активные вызовы эффектов и функции очистки.
Owner - низкоуровневый примитив
Для доменной сущности - у которой есть данные, id и место в списках - берите модели: model и staticModel строятся на той же семантике времени жизни и добавляют коллекции, разбор и сериализацию JSON, связи, запросы и onCleanup на экземплярах. Голый owner оставьте служебным подсистемам, которые сущностями не являются: сокетам, плеерам, кешам, фоновым процессам. Сравнение - на странице Виды моделей.
Динамическая работа под владельцем
Владелец дает динамической работе один жизненный цикл. Все, что создано внутри owner, можно удалить вместе.
import { event, onCleanup, owner, reaction, store } from "@virentia/core";
export function createDraftModel() {
return owner(() => {
const changed = event<string>();
const text = store("");
reaction({
on: changed,
run(value) {
text.value = value;
},
});
return { changed, text };
});
}owner добавляет dispose() на корневой объект модели. Когда черновик закрывается, вызовите dispose. Реакции, созданные внутри владельца, будут отвязаны вместе с ним.
const draft = createDraftModel();
draft.dispose();Если среда выполнения поддерживает using и Symbol.dispose, можно доверить очистку блоку кода:
{
using draft = createDraftModel();
// работа с draft
}WARNING
Symbol.dispose и using еще не одинаково доступны во всех JavaScript runtimes, включая Safari без транспиляции. Если ваш runtime, bundler или транспайлер их не поддерживает, используйте обычный model.dispose().
Вложенные владельцы уходят вместе с родителем
Владелец, созданный внутри тела другого владельца, — его ребёнок. Роспуск родителя распускает всех детей, начиная с самого вложенного: так cleanup ребёнка ещё может опираться на то, что держит родитель.
const screen = owner(() => {
const list = owner(() => {
reaction({ on: filtersChanged, run: refetch });
return {};
});
return { list };
});
screen.dispose(); // `list` тоже распущен — его реакция отцепленаРебёнка по-прежнему можно распустить отдельно, родителя это не затронет. Любой dispose идемпотентен, поэтому распустить сначала ребёнка вручную, а потом родителя — безопасно.
Без каскада подмодель пережила бы фичу, которая её создала: её реакции остались бы подписаны на глобальные сторы, а эффекты — в полёте.
Роспуск отменяет вызовы, сделанные моделью
Роспуск владельца отменяет вызовы эффектов, сделанные под ним, а не только вызовы эффектов, объявленных внутри него. Это важно, потому что эффекты обычно объявляются один раз на уровне модуля и вызываются из многих моделей:
const loadUserFx = effect(async (id: string) => api.get(id)); // уровень модуля
const screen = owner(() => {
void loadUserFx("u1"); // вызов принадлежит этому владельцу
return {};
});
screen.dispose(); // запрос отменяется с Error("Effect caller disposed")Отменяются только собственные вызовы этого владельца — параллельный вызов того же эффекта из другой модели продолжает работать. Вызов, сделанный без активного владельца, ничей, и роспуск его не трогает.
Внешняя очистка
Используйте onCleanup для работы, о которой Virentia не может знать сама.
const timerModel = owner(() => {
const timer = setInterval(() => {}, 1000);
onCleanup(() => {
clearInterval(timer);
});
return {};
});Используйте withOwner, когда вспомогательная функция должна привязать очистку к уже существующему владельцу. Он временно делает этого владельца текущим на время переданной функции, поэтому onCleanup внутри нее регистрируется на жизненный цикл модели.
import { onCleanup, owner, withOwner, type Owner } from "@virentia/core";
const model = owner((dispose, modelOwner) => {
return { dispose, owner: modelOwner };
});
function connectSocket(modelOwner: Owner) {
withOwner(modelOwner, () => {
const socket = new WebSocket("/events");
onCleanup(() => {
socket.close();
});
});
}
connectSocket(model.owner);Так вспомогательная функция остается переиспользуемой и не отвечает за весь жизненный цикл модели.
Владельцы нужны не только для борьбы с утечками. Они делают решение о жизненном цикле видимым: эта модель временная, эта работа принадлежит ей, а здесь ей разрешено исчезнуть.