Select slice 由 NgRx Store 中的几个实体组成

Select slice consisted of several entities in NgRx Store

我使用 NgRx 实体为由日志实体组成的“logs”减速器创建状态:EntityState<Log>。然后我想从我的 Angular 组件订阅几个日志实体。如果只有一个日志,我会使用:

this.store$
  .select(appStore => appStore.logs.entities[myLogId])
  .subscribe(log => someExpensiveOperation())

如何 select 多个实体并确保订阅仅在其中一个实体发生更改时触发一次?

这比听起来要复杂一些。我尝试了两个方向。

首先是使用 map 运算符过滤列表。只要列表中的任何实体发生变化,就会调用地图,因此您必须在它后面有一个运算符来忽略重复项。由于地图每次都将创建一个新数组,因此您不能使用标准 distinct* 运算符将其过滤掉。我创建了一个名为 distinctElements 的自定义运算符,它基本上是 distinctUntilChanged 但它对数组元素而不是数组本身进行引用检查。对于此示例,我假设您正在使用由实体适配器生成的 selectAll 选择器。它公开了所有实体的数组。

const { Observable, BehaviorSubject } = rxjs;
const { startWith, pairwise, filter, map, tap } = rxjs.operators;

function distinctElements(){
    return (source) => source.pipe(
        startWith(null),
        pairwise(),
        filter(([a, b]) => a == null || a.length !== b.length || a.some(x => !b.includes(x))),
        map(([a, b]) => b)
    );
};

let state = [
  { id: 1, value: 'a' },
  { id: 2, value: 'b' },
  { id: 3, value: 'c' }
];
const store$ = new BehaviorSubject(state);

const ids = [1, 3];
store$.pipe(
  map(entities => entities.filter(entity => ids.includes(entity.id))),
  distinctElements()
).subscribe((entities) => { console.log('next', entities); });

setTimeout(() => {
  state = [...state, { id: 4, value: 'd' }];
  console.log('add entity (4)');
  store$.next(state);
}, 10);

setTimeout(() => {
  state[0].value = 'aa';
  state = [{...state[0]}, ...state.slice(1)];
  console.log('update entity (1)');
  store$.next(state);
}, 1000);

setTimeout(() => {
  state = [...state.slice(0, 1), ...state.slice(2)];
  console.log('remove entity (2)');
  store$.next(state);
}, 2000);
<script src="https://unpkg.com/rxjs@rc/bundles/rxjs.umd.min.js"></script>

第二种选择是为每个实体创建单独的可观察对象,并对所有实体执行 combineLatest。对于此示例,我假设您正在使用由实体适配器生成的 selectEntities 选择器。这一个公开了一个对象,该对象可由实体的 id 索引。

const { Observable, BehaviorSubject, combineLatest, timer } = rxjs;
const { startWith, pairwise, filter, map, debounce, distinctUntilChanged } = rxjs.operators;

function distinctElements(){
    return (source) => source.pipe(
        startWith(null),
        pairwise(),
        filter(([a, b]) => a == null || a.length !== b.length || a.some(x => !b.includes(x))),
        map(([a, b]) => b)
    );
};

let state = {
  1: { id: 1, value: 'a' },
  2: { id: 2, value: 'b' },
  3: { id: 3, value: 'c' }
};
const store$ = new BehaviorSubject(state);

const ids = [1, 3];
combineLatest(
  ids.map(id => store$.pipe(
    map(entities => entities[id]),
    distinctUntilChanged()
  ))
).pipe(
  debounce(() => timer(0))
).subscribe((entities) => { console.log('next', entities); });

setTimeout(() => {
  state = { ...state, 4: { id: 4, value: 'd' } };
  console.log('add entity (4)');
  store$.next(state);
}, 10);

setTimeout(() => {
  state[1].value = 'aa';
  state = { ...state, 1: {...state[1]} };
  console.log('update entity (1)');
  store$.next(state);
}, 1000);

setTimeout(() => {
  state = { ...state };
  delete state[2];
  console.log('remove entity (2)');
  store$.next(state);
}, 2000);
<script src="https://unpkg.com/rxjs@rc/bundles/rxjs.umd.min.js"></script>

两者完成相同的事情,但方式不同。我没有进行任何类型的性能测试来确定哪个更好,但这可能取决于您选择的实体数量相对于列表的总大小。如果我不得不猜测,我会假设第一个性能更高。

关于选择结合多个切片的投影数据切片,您可以参考此答案:Denormalizing ngrx store- setting up selectors?