Angular 2,ngrx/store,RxJS和树状数据

Jim*_*mit 27 javascript rxjs ngrx angular

我一直试图找出一种方法来使用select运算符与rxjs的其他运算符一起查询树数据结构(在存储中规范化为平面列表),以便保留ChangeDetectionStrategy.OnPush的引用完整性语义,但我最好的尝试导致整个树在树的任何部分发生变化时被重新渲染.有没有人有任何想法?如果您将以下界面视为商店中数据的代表:

export interface TreeNodeState {
 id: string;
 text: string;
 children: string[] // the ids of the child nodes
}
export interface ApplicationState {
 nodes: TreeNodeState[]
}
Run Code Online (Sandbox Code Playgroud)

我需要创建一个选择器,对上面的状态进行非规范化,以返回实现以下接口的对象图:

export interface TreeNode {
 id: string;
 text: string;
 children: TreeNode[]
}
Run Code Online (Sandbox Code Playgroud)
也就是说,我需要一个带有Observable <ApplicationState>的函数并返回一个Observable <TreeNode []>,这样每个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
            ));
}
Run Code Online (Sandbox Code Playgroud)

cor*_*lla 2

如果愿意重新考虑这个问题,你可以使用 Rxjs 运算符scan

  1. 如果先前的 ApplicationState 不存在,则接受第一个。递归地将其转换为 TreeNodes。由于这是一个实际的对象,因此不涉及 rxjs。
  2. 每当接收到新的应用程序状态时,即当扫描触发时,实现一个使用接收到的状态改变先前节点的函数,并返回扫描运算符中的先前节点。这将保证您的引用完整性。
  3. 现在您可能会遇到一个新问题,因为可能无法拾取对变异树节点的更改。如果是这样,请通过为每个节点创建签名来查看跟踪,或者考虑向节点添加changeDetectorRef(由组件渲染节点提供),从而允许您标记要更新的组件。这可能会表现得更好,因为您可以使用更改检测策略 OnPush

伪代码:

state$.scan((state, nodes) => nodes ? mutateNodesBy(nodes, state) : stateToNodes(state))
Run Code Online (Sandbox Code Playgroud)

由于节点构建一次,然后仅发生变化,因此保证输出保持引用完整性(如果可能)。