RXJ рекурсивно разверните дерево

#rxjs

Вопрос:

У меня есть дерево узлов, каждый раз, когда пользователь «расширяет» узел, вызывается http-запрос, чтобы получить его дочерние элементы.

дерево узлов

Я ищу конвейер RXJS для рекурсивного расширения всех узлов дерева и создания расширенного дерева.

( Демо-версия StackBlitz )

То, как я это делаю сейчас, состоит в том, чтобы изменить источник и вывести измененный объект, когда это будет сделано. Есть ли для меня способ упростить этот ад ? возможно, без мутирующего источника .

 // 'ROOT' nodes
const rootNodes = [{ id: 0, parent: true }, { id: 1000, parent: true }];

// map the root nodes to a recursive function that mutate node and returns its children observables
const getChildrenOfRoot$ = rootNodes.map(node => getChildren(node));

// call the observables, in parallels, when done, print the *** mutated *** source.
forkJoin(...getChildrenOfRoot$).subscribe(() => console.log(rootNodes));


function getChildren(node: Node): Observable<Node[]> {

  return getChildrenFromServer(node.id).pipe(

    // if no children returned, don't continue
    filter((children: Node[]) => !!(children?.length)),

    // mutate argument's .children property with the returned children array.
    tap((children: Node[]) => (node.children = children)),

    mergeMap((children: Node[]) => {
      // mape children to observables returning either their children, or, an empty array, based on 'parent' property.
      const getChildrenOfChildren$ = children.map(c => (c.parent ? getChildren(c) : of([])));

      return forkJoin(...getChildrenOfChildren$);
    })
  );
}
 

Ответ №1:

TLDR;

Приложение StackBlitz.


Подробное объяснение

Поскольку мы имеем дело с рекурсией, я подумал, что было бы проще, если бы мы начали с базового случая.
Давайте посмотрим, что у нас есть изначально:

 const rootNodes = [{ id: 0, parent: true }, { id: 1000, parent: true }];
 

Предполагая, что это единственные родители в этом дереве(т. е. все их потомки будут конечными узлами), наша проблема становится проще: для каждого узла в приведенном выше массиве мы должны извлечь его дочерние элементы и добавить новое свойство к их узлам, называемое children .
Для этого давайте создадим processNodes детей:

 function processNodes (nodes: Node[]): Observable<Node[]> {
  return nodes.length 
    ? forkJoin(
        nodes.map(node => node.parent ? processNode(node) : of(node))
      )
    : of([]);
}
 

Обратите внимание, что мы также рассматриваем случай, когда массив узлов пуст, поэтому в этом случае мы просто возвращаем пустой массив( of([]) ). Давайте теперь сосредоточимся на этой части:

 forkJoin(
  nodes.map(node => node.parent ? processNode(node) : of(node))
)
 

Если аргумент processNodes был:

 [{ id: 0, parent: true }, { id: 1000, parent: true }, { id: 7, parent: false }]
 

тогда ожидаемый результат будет:

 [
  { id: 0, parent: true, children: [/* ... */] },
  { id: 1000, parent: true, children: [/* ... */] },
  { id: 7, parent: false }
]
 

Итак, если узел является родительским, то мы разделили логику, инкапсулированную в processNode функцию:

 function processNode (node: Node) {
  return getChildrenFromServer(node.id).pipe(
    mergeMap((children: Node[]) => processNodes(children)),
    map((children: Node[]) => ({ ...node, ...children.length amp;amp; { children } })),
  );
}
 

и вот здесь происходит рекурсия. Учитывая дочерние узлы некоторого родительского узла, мы повторяем процесс и в конце добавляем children свойство к родительскому узлу.

Итак, чтобы собрать все части вместе:

 processNodes(rootNodes).subscribe(console.log)

function processNodes (nodes: Node[]): Observable<Node[]> {
  return nodes.length 
    ? forkJoin(
        nodes.map(node => node.parent ? processNode(node) : of(node))
      )
    : of([]);
}

function processNode (node: Node) {
  return getChildrenFromServer(node.id).pipe(
    mergeMap((children: Node[]) => processNodes(children)),
    map((children: Node[]) => ({ ...node, ...children.length amp;amp; { children } })),
  );
}