#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;
Подробное объяснение
Поскольку мы имеем дело с рекурсией, я подумал, что было бы проще, если бы мы начали с базового случая.
Давайте посмотрим, что у нас есть изначально:
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 } })),
);
}