Angular 2,ngrx/store,RxJS和树状数据
问题描述:
我一直在想方设法将select操作符与rxjs的其他操作符结合使用来查询树数据结构(在存储中标准化为扁平列表)以这种方式保留ChangeDetectionStrategy.OnPush语义的参照完整性,但我最好的尝试是在树的任何部分发生变化时使整个树被重新渲染。有没有人有任何想法? 如果考虑下面的接口为代表的数据在商店:Angular 2,ngrx/store,RxJS和树状数据
export interface TreeNodeState {
id: string;
text: string;
children: string[] // the ids of the child nodes
}
export interface ApplicationState {
nodes: TreeNodeState[]
}
我需要创建denormalizes上面的状态返回实现以下接口对象的图形的选择:
export interface TreeNode {
id: string;
text: string;
children: TreeNode[]
}
理想情况下,我想让图的任何一部分只更新其子节点,如果它们发生更改,而不是在任何节点更改时返回全新图。 有谁知道如何使用ngrx/store和rxjs构建这样的选择器?
因为我已经尝试看看下面的代码片段的各种事物的更具体的例子:
// This is the implementation I'm currently using.
// It works but causes the entire tree to be rerendered
// when any part of the tree changes.
export function getSearchResults(searchText: string = '') {
return (state$: Observable<ExplorerState>) =>
Observable.combineLatest(
state$.let(getFolder(undefined)),
state$.let(getFolderEntities()),
state$.let(getDialogEntities()),
(root, folders, dialogs) =>
searchFolder(
root,
id => folders ? folders.get(id) : null,
id => folders ? folders.filter(f => f.parentId === id).toArray() : null,
id => dialogs ? dialogs.filter(d => d.folderId === id).toArray() : null,
searchText
)
);
}
function searchFolder(
folder: FolderState,
getFolder: (id: string) => FolderState,
getSubFolders: (id: string) => FolderState[],
getSubDialogs: (id: string) => DialogSummary[],
searchText: string
): FolderTree {
console.log('searching folder', folder ? folder.toJS() : folder);
const {id, name } = folder;
const isMatch = (text: string) => !!text && text.toLowerCase().indexOf(searchText) > -1;
return {
id,
name,
subFolders: getSubFolders(folder.id)
.map(subFolder => searchFolder(
subFolder,
getFolder,
getSubFolders,
getSubDialogs,
searchText))
.filter(subFolder => subFolder && (!!subFolder.dialogs.length || isMatch(subFolder.name))),
dialogs: getSubDialogs(id)
.filter(dialog => dialog && (isMatch(folder.name) || isMatch(dialog.name)))
} as FolderTree;
}
// This is an alternate implementation using recursion that I'd hoped would do what I wanted
// but is flawed somehow and just never returns a value.
export function getSearchResults2(searchText: string = '', folderId = null)
: (state$: Observable<ExplorerState>) => Observable<FolderTree> {
console.debug('Searching folder tree', { searchText, folderId });
const isMatch = (text: string) =>
!!text && text.search(new RegExp(searchText, 'i')) >= 0;
return (state$: Observable<ExplorerState>) =>
Observable.combineLatest(
state$.let(getFolder(folderId)),
state$.let(getContainedFolders(folderId))
.flatMap(subFolders => subFolders.map(sf => sf.id))
.flatMap(id => state$.let(getSearchResults2(searchText, id)))
.toArray(),
state$.let(getContainedDialogs(folderId)),
(folder: FolderState, folders: FolderTree[], dialogs: DialogSummary[]) => {
console.debug('Search complete. constructing tree...', {
id: folder.id,
name: folder.name,
subFolders: folders,
dialogs
});
return Object.assign({}, {
id: folder.id,
name: folder.name,
subFolders: folders
.filter(subFolder =>
subFolder.dialogs.length > 0 || isMatch(subFolder.name))
.sort((a, b) => a.name.localeCompare(b.name)),
dialogs: dialogs
.map(dialog => dialog as DialogSummary)
.filter(dialog =>
isMatch(folder.name)
|| isMatch(dialog.name))
.sort((a, b) => a.name.localeCompare(b.name))
}) as FolderTree;
}
);
}
// This is a similar implementation to the one (uses recursion) above but it is also flawed.
export function getFolderTree(folderId: string)
: (state$: Observable<ExplorerState>) => Observable<FolderTree> {
return (state$: Observable<ExplorerState>) => state$
.let(getFolder(folderId))
.concatMap(folder =>
Observable.combineLatest(
state$.let(getContainedFolders(folderId))
.flatMap(subFolders => subFolders.map(sf => sf.id))
.concatMap(id => state$.let(getFolderTree(id)))
.toArray(),
state$.let(getContainedDialogs(folderId)),
(folders: FolderTree[], dialogs: DialogSummary[]) => Object.assign({}, {
id: folder.id,
name: folder.name,
subFolders: folders.sort((a, b) => a.name.localeCompare(b.name)),
dialogs: dialogs.map(dialog => dialog as DialogSummary)
.sort((a, b) => a.name.localeCompare(b.name))
}) as FolderTree
));
}
答
如果愿意重新考虑这个问题,你可以使用Rxjs操作scan:
- 如果以前的ApplicationState不存在,请接受第一个。递归地将其转换为TreeNodes。由于这是一个实际的对象,因此不涉及rxjs。
- 每当接收到新的应用程序状态,即当扫描火灾,实现一个功能变异使用状态之前的节点接收,并在扫描操作返回以前的节点。这将保证您的参考完整性。
- 您现在可能会留下一个新问题,因为对变异树节点的更改可能不会被拾取。如果是这样,请通过为每个节点签名来查看track by,或者考虑将changeDetectorRef添加到节点(由组件渲染节点提供),从而允许您标记要更新的组件。这可能会更好,因为你可以用change detection strategy OnPush。
伪代码:
state$.scan((state, nodes) => nodes ? mutateNodesBy(nodes, state) : stateToNodes(state))
的输出保证保存参照完整性(在可能情况下)作为节点内置一次,然后只发生突变。
你有没有运气实现这个?我在我的应用程序中有相同的要求。 –
我们可以假定ApplicationState.nodes在父节点的子节点之前有父节点吗? – 0xcaff
此外,只有在对属性的引用发生更改(或调用了“markForCheck()”,但更新了整个组件后才会传播“OnPush”更改。这意味着你将不得不更新对数组的引用,重建会导致整个树被检查。您可能想使用Immutable.js,而不是OnPush,但我不确定它是如何与angular配合使用的。 – 0xcaff