2016-08-23 93 views
23

我一直在想方設法將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

回答

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)) 

的輸出保證保存參照完整性(在可能情況下)作爲節點內置一次,然後只發生突變。