BehaviorSubject отправляет одинаковые ссылки на состояние всем подписчикам - PullRequest
2 голосов
/ 20 июня 2019

В нашем одностраничном приложении мы разработали класс централизованного хранилища, который использует объект поведения RxJS для обработки состояния нашего приложения и всех его мутаций.Несколько компонентов в нашем приложении подписываются на тему поведения нашего магазина, чтобы получать любые обновления текущего состояния приложения.Это состояние затем привязывается к пользовательскому интерфейсу, чтобы при каждом изменении состояния пользовательский интерфейс отражал эти изменения.Всякий раз, когда компонент хочет изменить часть состояния, мы вызываем функцию, предоставляемую нашим хранилищем, которая выполняет требуемую работу и обновляет состояние, вызывающее следующее для субъекта поведения.Пока ничего особенного.(Мы используем Aurelia в качестве платформы, которая выполняет двухстороннее связывание)

Проблема, с которой мы сталкиваемся, заключается в том, что как только компонент меняет свою локальную переменную состояния, которую он получает из хранилища, другие компоненты обновляются, даже еслиnext () не вызывался для самого субъекта.

Мы также пытались подписаться на наблюдаемую версию субъекта, так как наблюдаемый должен отправлять разные копии данных всем подписчикам, но похоже, что это не так.case.

Похоже, что все подписчики-субъекты получают ссылку на объект, сохраненный в субъекте поведения.

import { BehaviorSubject, of } from 'rxjs'; 

const initialState = {
  data: {
    id: 1, 
    description: 'initial'
  }
}

const subject = new BehaviorSubject(initialState);
const observable = subject.asObservable();
let stateFromSubject; //Result after subscription to subject
let stateFromObservable; //Result after subscription to observable

subject.subscribe((val) => {
  console.log(`**Received ${val.data.id} from subject`);
  stateFromSubject = val;
});

observable.subscribe((val) => {
  console.log(`**Received ${val.data.id} from observable`);
  stateFromObservable = val;
});

stateFromSubject.data.id = 2;
// Both stateFromObservable and subject.getValue() now have a id of 2.
// next() wasn't called on the subject but its state got changed anyway

stateFromObservable.data.id = 3;
// Since observable aren't bi-directional I thought this would be a possible solution but same applies and all variable now shows 3

Я сделал стекаблитц с приведенным выше кодом.https://stackblitz.com/edit/rxjs-bhkd5n

Единственный обходной путь, который у нас есть, - это клонирование содержимого в некоторых наших подписчиках, где мы поддерживаем редакцию посредством связывания, как показано ниже:

observable.subscribe((val) => {
  stateFromObservable = JSON.parse(JSON.stringify(val));
});

Но это больше похоже навзломать, чем реальное решение.Должен быть лучший способ ...

Ответы [ 2 ]

3 голосов
/ 21 июня 2019

Да, все подписчики получают один и тот же экземпляр объекта в субъекте поведения, то есть как работают субъекты поведения. Если вы собираетесь мутировать объекты, вам нужно их клонировать.

Я использую эту функцию для клонирования своих объектов, которые я собираюсь связать с угловыми формами

const clone = obj =>
  Array.isArray(obj)
    ? obj.map(item => clone(item))
    : obj instanceof Date
    ? new Date(obj.getTime())
    : obj && typeof obj === 'object'
    ? Object.getOwnPropertyNames(obj).reduce((o, prop) => {
        o[prop] = clone(obj[prop]);
        return o;
      }, {})
    : obj;

Таким образом, если у вас есть наблюдаемые данные $, вы можете создать наблюдаемый клон $, где подписчики на эту наблюдаемую копию получат клон, который можно мутировать, не затрагивая другие компоненты.

clone$ = data$.pipe(map(data => clone(data)));

Таким образом, компоненты, которые просто отображают данные, могут подписаться на данные $ для эффективности, а компоненты, которые будут мутировать данные, могут подписаться на клон $.

Прочитайте в моей библиотеке Angular https://github.com/adriandavidbrand/ngx-rxcache и мою статью о ней https://medium.com/@adrianbrand/angular-state-management-with-rxcache-468a865fc3fb, в ней говорится о необходимости клонировать объекты, чтобы мы не мутировали данные, которые мы связываем с формами.

Похоже, цели вашего магазина те же, что и в моей библиотеке управления состоянием Angular. Это может дать вам некоторые идеи.

Я не знаком с Aurelia или, если у него есть каналы, но эта функция клона доступна в магазине, где мои данные отображаются с помощью клона $ observable, а в шаблонах - с клоном, который можно использовать, например,

data$ | clone as data

Важной частью является знание, когда клонировать, а не клонировать. Вам нужно только клонировать, если данные будут видоизменяться. Было бы действительно неэффективно клонировать массив данных, которые будут отображаться только в сетке.

1 голос
/ 20 июня 2019

Единственный обходной путь, который у нас есть, это клонирование состояния в некоторых наших подписчиках, где мы поддерживаем редакцию, через привязку, как показано ниже:

Не думаю, что смогу ответить, не переписав ваш магазин.

const initialState = {
  data: {
    id: 1, 
    description: 'initial'
  }
}

Этот объект состояния имеет глубоко структурированные данные. Каждый раз, когда вам нужно изменить состояние, объект должен быть реконструирован.

В качестве альтернативы,

const initialState = {
   1: {id: 1, description: 'initial'},
   2: {id: 2, description: 'initial'},
   3: {id: 3, description: 'initial'},
   _index: [1, 2, 3]
};

Это примерно как глубина объекта состояния, который я бы создал. Используйте пару ключ / значение для сопоставления между идентификаторами и значениями объекта. Теперь вы можете легко писать селекторы.

function getById(id: number): Observable<any> {
   return subject.pipe(
       map(state => state[id]),
       distinctUntilChanged()
   );
}

function getIds(): Observable<number[]> {
   return subject.pipe(
      map(state => state._index),
      distinctUntilChanged()
   );
}

Когда вы хотите изменить объект данных. Вы должны восстановить состояние, а также установить данные.

function append(data: Object) {
    const state = subject.value;
    subject.next({...state, [data.id]: Object.freeze(data), _index: [...state._index, data.id]});
}

function remove(id: number) {
    const state = {...subject.value};
    delete state[id];
    subject.next({...state, _index: state._index.filter(x => x !== id)});
}

Как только вы это сделаете. Вы должны заморозить нижестоящих потребителей вашего государственного объекта.

const subject = new BehaviorSubject(initialState);

function getStore(): Observable<any> {
   return subject.pipe(
      map(obj => Object.freeze(obj))
   );
}

function getById(id: number): Observable<any> {
   return getStore().pipe(
      map(state => state[id]),
      distinctUntilChanged()
   );
}

function getIds(): Observable<number[]> {
   return getStore().pipe(
      map(state => state._index),
      distinctUntilChanged()
   );
}

Позже, когда вы делаете что-то вроде этого:

stateFromSubject.data.id = 2;

Вы получите ошибку во время выполнения.

К вашему сведению: Выше написано в TypeScript

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...