Потеря скоупа
Работа юнитов в effector всегда происходит внутри скоупа — глобального или изолированного, созданного через fork()
. В глобальном случае контекст потерять невозможно, так как он используется по умолчанию. С изолированным скоупом всё сложнее: при потере скоупа операции начнут выполняться в глобальном режиме, а все обновления данных не попадут в скоуп в котором велась работа, и как следствие, клиенту отправится неконсистентное состояние.
Типичные места, где это проявляется:
setTimeout
/setInterval
addEventListener
WebSocket
- прямой вызов промисов в эффектах
- сторонние библиотеки с асинхронными API или колбэки.
Пример проблемы
Мы создадим простой таймер на React, хотя такая же модель поведения при потере скоупа будет соответствовать для любого фреймворка или среды:
import React from "react";import { createEvent, createStore, createEffect, scopeBind } from "effector";import { useUnit } from "effector-react";
const tick = createEvent();const $timer = createStore(0);
$timer.on(tick, (s) => s + 1);
export function Timer() { const [timer, startTimer] = useUnit([$timer, startTimerFx]);
return ( <div className="App"> <div>Timer:{timer} sec</div> <button onClick={startTimer}>Start timer</button> </div> );}
import React from "react";import { Provider } from "effector-react";import { fork } from "effector";import { Timer } from "./timer";
export const scope = fork();
export default function App() { return ( <Provider value={scope}> <Timer /> </Provider> );}
Теперь добавим эффект, который каждую секунду вызывает tick
:
const startTimerFx = createEffect(() => { setInterval(() => { tick(); }, 1000);});
Вот здесь можно потыкать пример.
На первый взгляд мы написали вполне рабочий код, но если запустить таймер, то вы заметите, что UI не обновляется. Это из-за того, что изменения таймера происходят в глобальном скоупе, а наше приложение работает в изолированном, который мы передали в <Provider>
, вы можете это заметить по логам в консоли.
Как исправить потерю скоупа ?
Чтобы исправить исправить потерю скоупа нужно использовать функцию scopeBind
. Этот метод возвращает функцию, привязанную к скоупу в котором метод был вызван, которую в последствии можно безопасно вызывать:
const startTimerFx = createEffect(() => { const bindedTick = scopeBind(tick);
setInterval(() => { bindedTick(); }, 1000);});
Заметьте, что метод scopeBind
сам умеет работать с текущим используемым скоупом. Однако, если вам нужно, то вы можете передать нужный скоуп в конфигурационный объект вторым аргументом.
scopeBind(tick, { scope });
Не забывайте очищать setInterval
после завершения работы во избежания утечек памяти. Очищать setInterval
можно отдельным эффектом, предварительно вернув из первого эффекта его id и сохранив в отдельный стор.
Почему происходит потеря скоупа?
Давайте представим, то как работает скоуп в effector:
// наш активный скоупlet scope;
function process() { try { scope = "effector"; asyncProcess(); } finally { scope = undefined; console.log("наш скоуп undefined"); }}
async function asyncProcess() { console.log("у нас есть скоуп", scope); // effector
await 1;
// тут мы уже потеряли контекст console.log("а здесь скоупа уже нет ", scope); // undefined}
process();
// Вывод:// у нас есть скоуп effector// наш скоуп undefined// а здесь скоупа уже нет undefined
Возможно вас интересует вопрос “Это проблема именно эффектора?”, однако это общий принцип работы с асинхронностью в JavaScript, все технологии, которые сталкиваются с необходимостью сохранения контекста в котором происходят вызовы так или иначе обходят это затруднение. Самый характерный пример это zone.js,
который для сохранения контекста оборачивает все асинхронные глобальные функции вроде setTimeout
или Promise.resolve
. Также способами решения этой проблемы бывает использование генераторов или ctx.schedule(() => asyncCall())
.
В JavaScript готовится proposal Async Context, который призван решить проблему потери контекста на уровне языка. Это позволит:
- Сохранять контекст автоматически через все асинхронные вызовы
- Избавиться от необходимости явного использования scopeBind
- Получить более предсказуемое поведение асинхронного кода
Как только это предложение войдет в язык и получит широкую поддержку, effector будет обновлен для использования этого нативного решения.
Связанные API и статьи
- API
Effect
- Описание эффекта, его методов и свойствScope
- Описание скоупа и его методовscopeBind
- Метод для привязки юнита к скоупуfork
- Оператор для создания скоупаallSettled
- Метод для вызова юнита в предоставленном скоупе и ожидания завершения всей цепочки эффектов
- Статьи
Документация на английском языке - самая актуальная, поскольку её пишет и обновляет команда effector. Перевод документации на другие языки осуществляется сообществом по мере наличия сил и желания.
Помните, что переведенные статьи могут быть неактуальными, поэтому для получения наиболее точной и актуальной информации рекомендуем использовать оригинальную англоязычную версию документации.