BehaviorSubject向所有订阅者发送相同的状态引用

时间:2019-06-20 18:09:43

标签: rxjs aurelia rxjs6

在我们的单页应用程序中,我们开发了一个集中的存储类,该类使用RxJS行为主题来处理应用程序的状态及其所有变异。我们的应用程序中的几个组件正在订阅我们商店的行为主题,以便接收对当前应用程序状态的任何更新。然后将此状态绑定到UI,以便每当状态更改时,UI都会反映这些更改。每当组件想要更改状态的一部分时,我们就会调用商店公开的函数来执行所需的工作,并在行为主体上更新下一步调用的状态。到目前为止,没有什么特别的。 (我们使用Aurelia作为执行2向绑定的框架)

我们面临的问题是,一旦组件更改了它从存储中收到的本地状态变量,即使未在子对象本身上调用next(),其他组件也会更新。

我们还尝试订阅可观察版本的主题,因为应将可观察版本的数据发送给所有订阅者不同的副本,但事实并非如此。

好像所有主题订阅者都在接收行为主题中存储的对象的引用。

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));
});

但是,这更像是一种破解,而不是真正的解决方案。一定有更好的方法...

3 个答案:

答案 0 :(得分:3)

是的,所有订阅者在行为主体中都收到对象的相同实例,这就是行为主体的工作方式。如果要突变对象,则需要克隆它们。

我使用此功能克隆要绑定到Angular表单的对象

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)));

因此,仅显示数据的组件可以订阅data $以提高效率,而将数据突变的组件可以订阅clone $。

在我的Angular https://github.com/adriandavidbrand/ngx-rxcache库和我的文章https://medium.com/@adrianbrand/angular-state-management-with-rxcache-468a865fc3fb上都读到了它,这涉及克隆对象的需要,这样我们就不会突变绑定到表单的数据。

听起来您商店的目标与我的Angular状态管理库相同。它可能会给您一些想法。

我不熟悉Aurelia或是否具有管道,但是该克隆功能可在商店中使用,通过可观察的clone $暴露我的数据,并在模板中使用可以像这样使用的克隆管道

data$ | clone as data

重要的部分是知道何时克隆而不是克隆。如果数据将要突变,则仅需要克隆。克隆仅将在网格中显示的数据数组真的是效率低下。

答案 1 :(得分:1)

  

到目前为止,我们唯一的解决方法是在某些我们支持版本的订户中克隆状态,如下所示:

如果不重写您的商店,我认为我无法回答。

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]
};

大约相当于我要创建的状态对象的。使用键/值对在ID和对象值之间进行映射。现在,您可以轻松编写选择器。

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编写

答案 2 :(得分:0)

您的示例的最大逻辑问题是,主题转发的对象实际上是单个对象引用。 RxJS不会为您创建克隆而开箱即用,这很好,否则默认情况下会在不需要时导致不必要的操作。

因此,尽管可以克隆订阅者接收到的值,但仍然无法保存对BehaviorSubject.getValue()的访问,该行为将返回原始引用。除了在状态的某些部分具有相同的引用实际上在很多方面都是有益的,例如可以将数组重用于多个显示组件,而不必从头开始重建它们。

您要做的是利用类似于Redux的单一来源真相模式,在这种情况下,您无需将确保订户获得克隆,而是将状态视为不可变对象。这意味着每次修改都会导致一个新状态。这进一步意味着您应该限制对动作的修改(Redux中的动作+归约器),以构造当前状态的新状态以及必要的更改并返回新副本。

现在所有这一切听起来可能需要做很多工作,但是您应该看看官方的Aurelia Store Plugin,它与您拥有几乎相同的概念,并确保带来了Redux的最佳创意到奥雷利亚的世界。