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

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[] 
 
}
也就是说,我需要一个函数,它接受一个O bservable <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 
 
      )); 
 
}

+0

你有没有运气实现这个?我在我的应用程序中有相同的要求。 –

+0

我们可以假定ApplicationState.nodes在父节点的子节点之前有父节点吗? – 0xcaff

+0

此外,只有在对属性的引用发生更改(或调用了“markForCheck()”,但更新了整个组件后才会传播“OnPush”更改。这意味着你将不得不更新对数组的引用,重建会导致整个树被检查。您可能想使用Immutable.js,而不是OnPush,但我不确定它是如何与angular配合使用的。 – 0xcaff

如果愿意重新考虑这个问题,你可以使用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)) 

的输出保证保存参照完整性(在可能情况下)作为节点内置一次,然后只发生突变。