2016-08-23 2 views
23

Я пытался выяснить, как использовать оператор select в сочетании с другими операторами rxjs для запроса структуры данных дерева (нормализованной в хранилище до плоский список) таким образом, чтобы он сохранял ссылочную целостность для семантики ChangeDetectionStrategy.OnPush, но мои лучшие попытки заставляют все дерево переизлучаться при изменении любой части дерева. У кого-нибудь есть идеи? Если вы считаете, следующий интерфейс в качестве представителя данных в хранилище:Угловые 2, ngrx/store, RxJS и древовидные данные

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[] 
 
}
То есть, мне нужна функция, которая принимает O bservable <ApplicationState> и возвращает наблюдаемый < 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

Кроме того, изменения 'OnPush' распространяются только после изменения ссылки на свойство (или вызывается' markForCheck() ', но обновляет весь компонент). Это означает, что вам нужно будет обновить ссылку на массив, чтобы восстановить все дерево. Вместо OnPush вы, вероятно, захотите использовать Immutable.js, но я не уверен, как именно это работает с угловым. – 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)) 

Выход гарантируется сохранение ссылочной целостности (где это возможно), как узлы построены один раз, то только мутантный.

Смежные вопросы