【问题标题】:Angular 2, ngrx/store, RxJS and tree-like dataAngular 2、ngrx/store、RxJS 和树状数据
【发布时间】:2016-12-30 03:14:15
【问题描述】:

我一直在尝试找出一种方法,将 select 运算符与 rxjs 的其他运算符结合使用来查询树数据结构(在存储中标准化为平面列表),以保持参考完整性ChangeDetectionStrategy.OnPush 语义,但我的最佳尝试会导致在树的任何部分发生更改时重新渲染整个树。有没有人有任何想法? 如果您认为以下接口代表存储中的数据:

export interface TreeNodeState {
 id: string;
 text: string;
 children: string[] // the ids of the child nodes
}
export interface ApplicationState {
 nodes: TreeNodeState[]
}

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

export interface TreeNode {
 id: string;
 text: string;
 children: TreeNode[]
}
也就是说,我需要一个接受 Observable 并返回 Observable 的函数,以便 每个 TreeNode 实例都保持引用完整性,除非它的一个子节点发生了变化

理想情况下,我希望图表的任何一部分仅在其子节点发生更改时才更新,而不是在任何节点更改时返回一个全新的图形。 有谁知道如何使用 ngrx/store 和 rxjs 构建这样的选择器?

有关我尝试过的各种事情的更具体示例,请查看下面的 sn-p:

// 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
            ));
}

【问题讨论】:

  • 你有没有运气实现这个?我的应用也有同样的要求。
  • 我们可以假设ApplicationState.nodes在父子节点之前有父节点吗?
  • 另外,OnPush 的更改仅在对属性的引用发生更改后传播(或调用 markForCheck(),但会更新整个组件)。这意味着您必须更新对数组的引用,重建导致检查整个树。而不是 OnPush,您可能想要使用 Immutable.js,但我不确定它与 angular 的工作原理。
  • 如果你让每个节点都成为可观察对象,而每个父节点都是观察者呢?

标签: javascript angular rxjs ngrx


【解决方案1】:

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

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

伪代码:

state$.scan((state, nodes) => nodes ? mutateNodesBy(nodes, state) : stateToNodes(state))

输出保证保持引用完整性(在可能的情况下),因为节点只构建一次,然后只发生变异。

【讨论】:

  • 你能提供一个演示吗?我不确定我是否完全理解您的想法。
猜你喜欢
  • 2016-12-28
  • 2018-03-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-03-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多