ngrx - createSelector 与 Observable.combineLatest
ngrx - createSelector vs Observable.combineLatest
我刚刚 运行 进入了 @ngrx
的 custom selectors,我对这个功能简直无法接受。
根据 books
对 selectedUser
的用例,我无法给出使用自定义选择器的真正充分理由,例如 :
export const selectVisibleBooks = createSelector(selectUser, selectAllBooks, (selectedUser: User, allBooks: Books[]) => {
return allBooks.filter((book: Book) => book.userId === selectedUser.id);
});
而不是像 :
export const selectVisibleBooks = Observable.combineLatest(selectUser, selectAllBooks, (selectedUser: User, allBooks: Books[]) => {
return allBooks.filter((book: Book) => book.userId === selectedUser.id);
});
我试图说服自己memoization of the createSelector
is the crucial part, but as far as I understood, it cannot perform these performance boosts to non-primitive values, so it wont really save any computing for non primitive slices, which by using Rx
's distinctUntilChanged
运算符加上combineLatest
是可以解决的
所以我错过了什么,为什么要使用@ngrx/selector
?
提前感谢您的任何见解。
也许它比记忆化更重要,但我没有看到任何在 source code. All that is advertised in the docs 中脱颖而出的是记忆化和一种重置它的方法,您基本上也可以使用不同的运算符来完成。我会说使用它的原因是它很方便。至少在简单的情况下,它比将不同的运算符绑定到 combineLatest
.
的每个输入更方便
另一个好处是它允许您集中与状态的内部结构相关的逻辑。您可以为其创建一个选择器并执行 store.select(selectBaz)
,而不是到处执行 store.select(x => foo.bar.baz)
。您可以将选择器组合到。通过这种方式,您只需要在一个地方设置遍历状态树的逻辑。如果您必须更改状态的结构,这是有益的,因为您只需在一个地方进行更改,而不用查找每个选择器。不过,每个人都可能不喜欢添加更多样板文件。但是作为一个必须对状态进行重大重构的人,我只使用选择器。
createSelector
虽然是非常基本的,所以您只能将它用于基本类型的操作。在您检索 objects 列表的情况下,您只需要过滤的子集,它就不够用了。这是一个例子:
const selectParentVmById = (id: string) => createSelector<RootState, Parent, Child[], ParentVm>(
selectParentById(id),
selectChildren(),
(parent: Parent, children: Child[]) => (<ParentVm>{
...parent,
children: children.filter(child => parent.children.includes(child.id))
})
);
在这种情况下,选择器 selectParentVmById
将在 selectChildren()
发出一个不同的数组时发出,如果其中的任何元素发生变化,就会发生这种情况。如果更改的元素是 parent 的 children 之一,那就太好了。如果不是,那么你会得到不必要的流失,因为记忆是在整个列表而不是过滤列表(或者更确切地说是其中的元素)上完成的。我有很多这样的场景,并且已经开始只使用 createSelector
作为简单的选择器并将它们与 combineLatest
结合起来并滚动我自己的记忆。
这不是一般不使用它的理由,您只需要了解它的局限性即可。
额外学分
你的问题与此无关,但自从我提出这个问题后,我想我会给出完整的解决方案。我开始使用名为 distinctElements()
的自定义运算符,它的作用类似于 distinctUntilChanged()
,但应用于列表中的元素而不是列表本身。
这里是运算符:
import { Observable } from 'rxjs/Observable';
import { startWith, pairwise, filter, map } from 'rxjs/operators';
export const distinctElements = () => <T extends Array<V>, V>(source: Observable<T>) => {
return source.pipe(
startWith(<T>null),
pairwise(),
filter(([a, b]) => a == null || a.length !== b.length || a.some(x => !b.includes(x))),
map(([a, b]) => b)
)
};
这里将重构上面的代码以使用它:
const selectParentVmById = (store: Store<RootState>, id: string): ParentVm => {
return store.select(selectParentById(id)).pipe(
distinctUntilChanged(),
switchMap((parent) => store.select(selectChildren()).pipe(
map((children) => children.filter(child => parent.children.includes(child.id))),
distinctElements(),
map((children) => <ParentVm> { ...parent, children })
))
);
}
需要更多的代码,但它减少了浪费的工作。您可以根据您的情况添加 shareReplay(1)
。
我刚刚 运行 进入了 @ngrx
的 custom selectors,我对这个功能简直无法接受。
根据 books
对 selectedUser
的用例,我无法给出使用自定义选择器的真正充分理由,例如 :
export const selectVisibleBooks = createSelector(selectUser, selectAllBooks, (selectedUser: User, allBooks: Books[]) => {
return allBooks.filter((book: Book) => book.userId === selectedUser.id);
});
而不是像 :
export const selectVisibleBooks = Observable.combineLatest(selectUser, selectAllBooks, (selectedUser: User, allBooks: Books[]) => {
return allBooks.filter((book: Book) => book.userId === selectedUser.id);
});
我试图说服自己memoization of the createSelector
is the crucial part, but as far as I understood, it cannot perform these performance boosts to non-primitive values, so it wont really save any computing for non primitive slices, which by using Rx
's distinctUntilChanged
运算符加上combineLatest
是可以解决的
所以我错过了什么,为什么要使用@ngrx/selector
?
提前感谢您的任何见解。
也许它比记忆化更重要,但我没有看到任何在 source code. All that is advertised in the docs 中脱颖而出的是记忆化和一种重置它的方法,您基本上也可以使用不同的运算符来完成。我会说使用它的原因是它很方便。至少在简单的情况下,它比将不同的运算符绑定到 combineLatest
.
另一个好处是它允许您集中与状态的内部结构相关的逻辑。您可以为其创建一个选择器并执行 store.select(selectBaz)
,而不是到处执行 store.select(x => foo.bar.baz)
。您可以将选择器组合到。通过这种方式,您只需要在一个地方设置遍历状态树的逻辑。如果您必须更改状态的结构,这是有益的,因为您只需在一个地方进行更改,而不用查找每个选择器。不过,每个人都可能不喜欢添加更多样板文件。但是作为一个必须对状态进行重大重构的人,我只使用选择器。
createSelector
虽然是非常基本的,所以您只能将它用于基本类型的操作。在您检索 objects 列表的情况下,您只需要过滤的子集,它就不够用了。这是一个例子:
const selectParentVmById = (id: string) => createSelector<RootState, Parent, Child[], ParentVm>(
selectParentById(id),
selectChildren(),
(parent: Parent, children: Child[]) => (<ParentVm>{
...parent,
children: children.filter(child => parent.children.includes(child.id))
})
);
在这种情况下,选择器 selectParentVmById
将在 selectChildren()
发出一个不同的数组时发出,如果其中的任何元素发生变化,就会发生这种情况。如果更改的元素是 parent 的 children 之一,那就太好了。如果不是,那么你会得到不必要的流失,因为记忆是在整个列表而不是过滤列表(或者更确切地说是其中的元素)上完成的。我有很多这样的场景,并且已经开始只使用 createSelector
作为简单的选择器并将它们与 combineLatest
结合起来并滚动我自己的记忆。
这不是一般不使用它的理由,您只需要了解它的局限性即可。
额外学分
你的问题与此无关,但自从我提出这个问题后,我想我会给出完整的解决方案。我开始使用名为 distinctElements()
的自定义运算符,它的作用类似于 distinctUntilChanged()
,但应用于列表中的元素而不是列表本身。
这里是运算符:
import { Observable } from 'rxjs/Observable';
import { startWith, pairwise, filter, map } from 'rxjs/operators';
export const distinctElements = () => <T extends Array<V>, V>(source: Observable<T>) => {
return source.pipe(
startWith(<T>null),
pairwise(),
filter(([a, b]) => a == null || a.length !== b.length || a.some(x => !b.includes(x))),
map(([a, b]) => b)
)
};
这里将重构上面的代码以使用它:
const selectParentVmById = (store: Store<RootState>, id: string): ParentVm => {
return store.select(selectParentById(id)).pipe(
distinctUntilChanged(),
switchMap((parent) => store.select(selectChildren()).pipe(
map((children) => children.filter(child => parent.children.includes(child.id))),
distinctElements(),
map((children) => <ParentVm> { ...parent, children })
))
);
}
需要更多的代码,但它减少了浪费的工作。您可以根据您的情况添加 shareReplay(1)
。