如何避免在rxjs中使用声明性方法使用订阅和BehaviorSubject.value



在重构我的angular应用程序时,我基本上想去掉所有订阅,以便只使用angular提供的async管道(只是一种声明性方法,而不是命令式方法)。

当多个源可能导致流中的更改时,我在实现声明性方法时遇到了问题。如果我们只有一个源,那么当然,我可以使用scan运算符来构建我的发射值。

场景

假设我只想有一个简单的组件,在路由过程中解析字符串数组。在组件中,我希望显示列表,并希望能够使用按钮添加或删除项目。

限制

  1. 我不想使用subscribe,因为我希望angular使用(async管道)来处理取消预订
  2. 我不想使用BehaviorSubject.value,因为(从我的角度来看)它是一种命令式方法,而不是声明式方法
  3. 实际上,我根本不想使用任何类型的主题(除了用于按钮click事件传播的主题),因为我认为没有必要。我应该已经拥有了所有需要的可观察性,它们只需要是";粘在一起">

目前的流程到目前为止,我的旅程经历了几个步骤。请注意,所有方法都很好,但每种方法都有各自的缺点):

  1. 使用BehaviorSubject.value创建新阵列-->非声明性
  2. 尝试scan运算符并创建一个Action接口,其中每个按钮都会发出一个类型为XY的操作。此操作将在传递给scan的函数中读取,然后使用开关来确定要采取的操作。这感觉有点像Redux,但在一个管道中混合不同的值类型是一种奇怪的感觉(首先是初始数组,然后是操作)
  3. 到目前为止,我最喜欢的方法是:我基本上通过使用shareReplay来模仿BehaviorSubject,并在我的按钮中使用这个即时发出的值,通过使用concatMap切换到一个新的可观察对象,其中我只取1个值,以防止创建循环。下面提到的实施示例:

list-view.component.html:

<ul>
<li *ngFor="let item of items$ | async; let i = index">
{{ item }} <button (click)="remove$.next(i)">remove</button>
</li>
</ul>
<button (click)="add$.next('test2')">add</button>

list-view.component.ts

// simple subject for propagating clicks to add button, string passed is the new entry in the array
add$ = new Subject<string>();
// simple subject for propagating clicks to remove button, number passed represents the index to be removed
remove$ = new Subject<number>();
// actual list to display
items$: Observable<string[]>;
constructor(private readonly _route: ActivatedRoute) {
// define observable emitting resolver data (initial data on component load)
// merging initial data, data on add and data on remove together and subscribe in order to bring data to Subject
this.items$ = merge(
this._route.data.pipe(map((items) => items[ITEMS_KEY])),
// define observable for adding items to the array
this.add$.pipe(
concatMap((added) =>
this.items$.pipe(
map((list) => [...list, added]),
take(1)
)
)
),
// define observable for removing items to the array
this.remove$.pipe(
concatMap((index) =>
this.items$.pipe(
map((list) => [...list.slice(0, index), ...list.slice(index + 1)]),
take(1)
)
)
)
).pipe(shareReplay(1));
}

尽管如此,我觉得这应该是最简单的例子,而且对于这种问题,我的实现似乎很复杂。如果有人能帮助找到解决这个简单问题的方法,那就太好了。

您可以在这里找到我的实现的StackBlitz示例:https://stackblitz.com/edit/angular-ivy-yj1efm?file=src/app/list-查看/列表查看组件.ts

您可以创建一个modifications$流,从您的";修改对象";,并将它们映射到一个函数,该函数将相应地修改状态:

export class AppComponent {
add$ = new Subject<string>();
remove$ = new Subject<number>();
private modifications$ = merge(
this.add$.pipe(map(item => state => state.concat(item))),
this.remove$.pipe(map(index => state => state.filter((_, i) => i !== index))),
);
private routeData$ = this.route.data.pipe(map(items => items[ITEMS_KEY]));
items$ = this.routeData$.pipe(
switchMap(items => this.modifications$.pipe(
scan((state, fn) => fn(state), items),
startWith(items)
))
);
constructor(private route: ActivatedRoute) { }
}

在这里,我们定义items$从路由数据开始,然后切换到将传入的reducer函数应用于状态的流。我们使用来自路由数据的初始items作为scan内的种子值。我们还使用CCD_ 18来初始发射初始CCD_。

这是一个StackBlitz的小样本。

首先,您不需要主题来传播HTML事件。

Angular在后台使用EventEmitter(基本上是一个主题)在整个应用程序中传播更改。

所以这个

<button (click)="remove$.next(i)">remove</button>

应该成为这个

<button (click)="removeItem(item, i)">remove</button>

接下来,对于路线数据,您可以使用简单的运算符为其创建主题

routeData$ = this._route.data.pipe(pluck('ITEMS_KEY'), shareReplay(1)),

现在,这为您的组件提供了一个更干净的代码:

routeData$ = this._route.data.pipe(pluck('ITEMS_KEY'), shareReplay(1)),
constructor(private readonly _route: ActivatedRoute) {}
addItem(item: any) {
// ...
}
removeItem(item: any) {
// ...
}

最后,您必须决定这将如何影响您的数据。addItemremoveItem应该做些什么?你有几个选项,例如:

  • 对API进行http调用
  • 更新您的应用程序状态
  • 重定向到相同的路由,但更新数据/路由参数等

有了这个,你应该能够取消所有订阅,让Angular为你做这项工作。

更好的是,您现在可以切换到OnPush检测策略,并大大提高您的应用程序性能!

这是我对它的看法:https://stackblitz.com/edit/angular-ivy-nsxabg?file=src%2Fapp%2Flist-查看%2List-view.component.ts

我认为使用Subject来模拟事件是一种过分的做法,如果你只为两个函数中的$observable项创建新的状态,你就有了一个巧妙的解决方案。

虽然我认为@BizzyBob的答案是最适用的(即回答问题,基于rxjs,易于理解),但我想添加我自己的带有状态管理库effector.dev 的版本

原因:当你需要一些外部资源参与时,它会再次变得复杂。不幸的是,这就是为什么我不再喜欢基于RxJS的状态管理器的原因。

我的效果器选项:演示

@Injectable()
export class ListViewEffectorService {
public items$ = createStore<string[]>([]);
public addItem = createEvent<string>();
public removeItem = createEvent<number>();
constructor(private readonly _route: ActivatedRoute, private ngZone: NgZone) {
this.ngZone.run(() => {
sample({
clock: fromObservable<string[]>(
this._route.data.pipe(map((items) => items[ITEMS_KEY] as string[]))
), // 1. when happened
source: this.items$, // 2. take from here
fn: (currentItems, newItems) => [...currentItems, ...newItems], // 3. convert
target: this.items$, // 4. and save
});
sample({
clock: this.addItem,
source: this.items$,
fn: (currentItems, newItem) => [...currentItems, newItem],
target: this.items$,
});
sample({
clock: this.removeItem,
source: this.items$,
fn: (currentItems, toRemove) =>
currentItems.filter((_, idx) => idx !== toRemove),
target: this.items$,
});
});
}
}

最新更新