行为主体向所有订阅者发送相同的状态引用



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

我们面临的问题是,一旦组件更改了它从存储接收的本地状态变量,即使 subejct 本身没有调用 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

到目前为止,我们唯一的解决方法是通过绑定来克隆某些订阅者中的 sate,我们支持版本,如下所示:

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

但这感觉更像是黑客攻击,而不是真正的解决方案。 一定有更好的方法...

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

我使用这个函数来克隆我要绑定到 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

重要的部分是知道何时克隆和不克隆。仅当数据要发生突变时,才需要克隆。克隆只会显示在网格中的数据数组非常低效。

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

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

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;

你将收到运行时错误。

仅供参考:以上是用打字稿写的

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

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

相反,您要做的是利用类似于 Redux 的单一事实来源模式,其中不是确保订阅者获得克隆,而是将状态视为不可变对象。这意味着每次修改都会导致新的状态。这进一步意味着您应该限制对操作(Redux 中的操作 + reducer )的修改,这些操作构造当前的新状态加上必要的更改并返回新副本。

现在所有这些听起来都像是很多工作,但你应该看看官方的Aurelia商店插件,它与你共享几乎相同的概念,并确保将Redux的最佳想法带到Aurelia的世界。

最新更新