2016-08-23 20 views
23

Bir ağaç veri yapısını sorgulamak için rxjs'in diğer operatörleri ile birlikte seçme işlecini kullanmanın bir yolunu bulmaya çalışıyorum. düz liste) ChangeDetectionStrategy.OnPush semantiklerinin referans bütünlüğünü koruyacak şekilde, ancak en iyi girişimlerim ağacın herhangi bir kısmı değiştiğinde tüm ağacın yeniden işlenmesine neden olur. Herhangi bir fikri olan var mı? mağazadan verilerin temsilcisi olarak aşağıdaki arayüz düşünün:Açısal 2, ngrx/mağaza, RxJS ve ağaç benzeri veriler

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

aşağıdaki arayüzü uygulayan nesnelerin bir grafiğini dönmek için yukarıdaki durumunu denormalizes bir seçici oluşturmanız gerekir :

export interface TreeNode { 
 
id: string; 
 
text: string; 
 
children: TreeNode[] 
 
}
olduğunu , ben bir O götüren bir işlevi gerekir bservable <ApplicationState> ve bir Gözlemci < TreeNode [] > döndürür, böylece her TreeNode örneği, çocuklarından biri değiştirmediği sürece başvuru bütünlüğünü korur.

İdeal olarak, grafiğin herhangi bir parçasının yalnızca, herhangi bir düğüm değiştiğinde tamamen yeni bir grafik döndürmek yerine, değiştiyse çocuklarını güncellemesini istiyorum. Ngrx/store ve rxjs kullanarak böyle bir seçicinin nasıl oluşturulacağını bilen var mı?

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

Bunu uygulamak için hiç şansınız oldu mu? Uygulamamda aynı şartı yaşıyorum. –

+0

"ApplicationState.nodes" öğesinin ebeveynlerin çocuk düğümlerinden önce ana düğümleri olduğunu varsayabilir miyiz? – 0xcaff

+0

Ayrıca, 'OnPush' değişiklikleri yalnızca bir özellik referansı değiştirildikten sonra yayılır (veya 'markForCheck()' çağrılır, ancak bu, tüm bileşeni güncelleştirir). Bu, dizinin referansını güncellemeniz ve yeniden yapılandırmanın tüm ağacın kontrol edilmesine neden olmanız anlamına gelir. OnPush yerine, muhtemelen Immutable.js'yi kullanmak istersiniz, ancak bunun açısal olarak tam olarak nasıl çalıştığından emin değilim. – 0xcaff

cevap

1

, sen Rxjs operatörü scan kullanabilirsiniz sorunu yeniden düşünmeye istekli ise: Ben aşağıdaki pasajı kontrol teşebbüs şeylerden türlü daha somut örnekler için

:

  1. Önceki bir ApplicationState yoksa, ilkini kabul edin. Yinelemeli olarak TreeNodes'a dönüştürün. Bu gerçek bir nesne olduğu için, rxjs dahil değildir.
  2. yeni bir uygulama durumu alındığında zaman, yani, tarama yangınlar, devlet kullanarak mutasyon önceki düğümler alınan bir fonksiyonu uygulamak ve tarama operatör önceki düğümleri döner. Bu size referans bütünlüğünü garanti edecektir.
  3. Artık mutasyona uğramış ağaç düğümlerindeki değişiklikler alınamayacağı için yeni bir sorunla karşı karşıya kalabilirsiniz. Eğer öyleyse, her bir düğüm için bir imza yaparak track by'a bakın veya bir bileşeni (bileşen oluşturma düğümü tarafından sağlanan) bir changeDetectorRef eklemeyi düşünerek, bir bileşeni güncelleme için işaretlemenizi sağlayın. Bu, change detection strategy OnPush ile gidebileceğinizden, muhtemelen daha iyi performans gösterecektir.

yalancı kod:

çıkış düğümleri kez inşa edildiği gibidir (mümkün olduğunda) başvuru bütünlüğü, o zaman yalnızca mutasyona tabi tutulmuş korumak için garanti
state$.scan((state, nodes) => nodes ? mutateNodesBy(nodes, state) : stateToNodes(state)) 

.