uitbreiden - recursief uitbreiden
De expand operator voert een recursieve transformatie uit die van elke waarde een nieuwe Observable genereert en het resultaat ook uitbreidt. Het is ideaal voor bewerkingen die waarden één voor één uitbreiden, zoals het doorlopen van boomstructuren, API-paginatie en recursieve berekeningen.
🔰 Basis syntaxis en gebruik
import { of } from 'rxjs';
import { expand, take } from 'rxjs';
// 2Recursief proces van verdubbelen
of(1).pipe(
expand(x => of(x * 2)),
take(5) // Preventie van oneindige lus
).subscribe(console.log);
// Uitvoer: 1, 2, 4, 8, 16Stroom van de bewerking: 1.
- een initiële waarde van
1wordt uitgegeven - De functie
expandontvangt1en geeftof(2)terug 3. De functieexpandwordt aangeroepen. 2wordt uitgegeven en de functieexpandwordt opnieuw aangeroepen 4. De functieexpandontvangt1en geeftof(2)terug- De functie
expandontvangt2en geeftof(4)terug 5. De functieexpandwordt opnieuw aangeroepen. - deze iteratie...
import { of } from 'rxjs';
import { expand, take } from 'rxjs';
// 2Recursief proces van verdubbelen
of(1).pipe(
expand(x => of(x * 2)),
take(5) // Preventie van oneindige lus
).subscribe(console.log);
// Uitvoer: 1, 2, 4, 8, 16
uitbreidenresulteert in een oneindige lus als er geen afsluitvoorwaarde is opgegeven. Zorg ervoor dat je een afsluitvoorwaarde instelt, zoalstakeof voorwaardelijkEMPTYretourneren.
🌐 Officiële RxJS documentatie - uitbreiden
Verschillen met mergeMap
expand is vergelijkbaar met mergeMap, behalve dat de resultaten van de gegenereerde Observable ook recursief worden verwerkt.
import { of } from 'rxjs';
import { mergeMap, expand, take } from 'rxjs';
const double = (x: number) => of(x * 2);
// mergeMap: 1Slechts eenmaal converteren
of(1).pipe(
mergeMap(double),
take(5)
).subscribe(console.log);
// Uitvoer: 2
// (1Slechts één waarde,2wordt niet opnieuw geconverteerd)
// expand: Recursief converteren
of(1).pipe(
expand(double),
take(5)
).subscribe(console.log);
// Uitvoer: 1, 2, 4, 8, 16
// (Elk resultaat wordt opnieuw omgezet)TABEL 14
💡 Typisch gebruikspatroon
1. recursie met beëindigingsvoorwaarden
import { of, EMPTY } from 'rxjs';
import { expand } from 'rxjs';
// 10naar minder dan2verdubbeld
of(1).pipe(
expand(x => {
const next = x * 2;
return next < 10 ? of(next) : EMPTY;
})
).subscribe(console.log);
// Uitvoer: 1, 2, 4, 8
// (16wordt geconverteerd naar10Aangezien het groter is dan of gelijk aanEMPTYwordt teruggegeven en het einde)2. doorlopen van boomstructuren
import { of, from, EMPTY } from 'rxjs';
import { expand, mergeMap } from 'rxjs';
interface TreeNode {
id: number;
name: string;
children?: TreeNode[];
}
const tree: TreeNode = {
id: 1,
name: 'Root',
children: [
{
id: 2,
name: 'Child 1',
children: [
{ id: 4, name: 'Grandchild 1' },
{ id: 5, name: 'Grandchild 2' }
]
},
{
id: 3,
name: 'Child 2',
children: [
{ id: 6, name: 'Grandchild 3' }
]
}
]
};
// De hele boom doorlopen
of(tree).pipe(
expand(node =>
node.children && node.children.length > 0
? from(node.children)
: EMPTY
)
).subscribe(node => {
console.log(`ID: ${node.id}, Name: ${node.name}`);
});
// Uitvoer:
// ID: 1, Name: Root
// ID: 2, Name: Child 1
// ID: 3, Name: Child 2
// ID: 4, Name: Grandchild 1
// ID: 5, Name: Grandchild 2
// ID: 6, Name: Grandchild 33. paginering van de API
import { of, EMPTY } from 'rxjs';
import { expand, mergeMap } from 'rxjs';
interface PageResponse {
data: string[];
nextPage: number | null;
}
function fetchPage(page: number): Promise<PageResponse> {
// APIVerzoek simuleren
return new Promise(resolve => {
setTimeout(() => {
if (page > 3) {
resolve({ data: [], nextPage: null });
} else {
resolve({
data: [`Item ${page}-1`, `Item ${page}-2`, `Item ${page}-3`],
nextPage: page + 1
});
}
}, 100);
});
}
// Alle pagina's opeenvolgend ophalen
of(1).pipe(
expand(page => {
return page > 0 ? of(page) : EMPTY;
}),
mergeMap(page => fetchPage(page)),
expand(response =>
response.nextPage
? of(response.nextPage).pipe(
mergeMap(nextPage => fetchPage(nextPage))
)
: EMPTY
)
).subscribe(response => {
console.log(`Gegevens van de pagina:`, response.data);
});Meer praktische uitvoering van paginering
import { defer, EMPTY, lastValueFrom } from 'rxjs';
import { expand, map, reduce, tap } from 'rxjs';
interface PaginatedResponse<T> {
items: T[];
nextCursor: string | null;
}
function fetchPagedData<T>(
fetchFn: (cursor: string | null) => Promise<PaginatedResponse<T>>
): Promise<T[]> {
return lastValueFrom(
defer(() => fetchFn(null)).pipe(
expand(response =>
response.nextCursor
? defer(() => fetchFn(response.nextCursor))
: EMPTY
),
map(response => response.items),
reduce((acc, items) => [...acc, ...items], [] as T[])
)
);
}
// UIAanmaken van elementen
const container = document.createElement('div');
document.body.appendChild(container);
const title = document.createElement('h3');
title.textContent = 'Voorbeeld implementatie paginering';
container.appendChild(title);
const button = document.createElement('button');
button.textContent = 'Alle gegevens ophalen';
container.appendChild(button);
const status = document.createElement('div');
status.style.marginTop = '10px';
status.style.padding = '10px';
status.style.backgroundColor = '#f0f0f0';
container.appendChild(status);
const output = document.createElement('pre');
output.style.marginTop = '10px';
output.style.padding = '10px';
output.style.backgroundColor = '#f9f9f9';
output.style.maxHeight = '300px';
output.style.overflow = 'auto';
container.appendChild(output);
// Gebruiksvoorbeeld:VoorbeeldAPIGebruikersgegevens ophalen met
interface User {
id: number;
name: string;
email: string;
}
// VoorbeeldAPISimuleer een
async function fetchUsers(cursor: string | null): Promise<PaginatedResponse<User>> {
// APISimuleer verzoeken (met100msVertraagd)
await new Promise(resolve => setTimeout(resolve, 100));
const page = cursor ? parseInt(cursor) : 1;
const pageSize = 5;
const totalPages = 4;
if (page > totalPages) {
return { items: [], nextCursor: null };
}
const items: User[] = Array.from({ length: pageSize }, (_, i) => ({
id: (page - 1) * pageSize + i + 1,
name: `User ${(page - 1) * pageSize + i + 1}`,
email: `user${(page - 1) * pageSize + i + 1}@example.com`
}));
return {
items,
nextCursor: page < totalPages ? String(page + 1) : null
};
}
// Alle gegevens ophalen met één klik op een knop
button.addEventListener('click', async () => {
button.disabled = true;
status.textContent = 'Gegevensverwerving bezig...';
output.textContent = '';
try {
const allUsers = await fetchPagedData(fetchUsers);
status.textContent = `Acquisitie voltooid: ${allUsers.length}Gebruikersgegevens voor`;
output.textContent = JSON.stringify(allUsers, null, 2);
console.log(`Aantal gebruikers: ${allUsers.length}`);
console.log('Gebruikersgegevens:', allUsers);
} catch (error) {
status.textContent = `Fout: ${error}`;
} finally {
button.disabled = false;
}
});🧠 Praktisch codevoorbeeld (weergave van directoryhiërarchie)
Dit is een voorbeeld van het recursief doorlopen van de mappenstructuur van een bestandssysteem.
import { of, from, EMPTY } from 'rxjs';
import { expand, tap } from 'rxjs';
interface FileSystemItem {
name: string;
type: 'file' | 'directory';
path: string;
children?: FileSystemItem[];
level: number;
}
// Structuur voorbeeldbestandssysteem
const fileSystem: FileSystemItem = {
name: 'root',
type: 'directory',
path: '/root',
level: 0,
children: [
{
name: 'src',
type: 'directory',
path: '/root/src',
level: 1,
children: [
{ name: 'index.ts', type: 'file', path: '/root/src/index.ts', level: 2 },
{ name: 'utils.ts', type: 'file', path: '/root/src/utils.ts', level: 2 },
{
name: 'components',
type: 'directory',
path: '/root/src/components',
level: 2,
children: [
{ name: 'Button.tsx', type: 'file', path: '/root/src/components/Button.tsx', level: 3 },
{ name: 'Input.tsx', type: 'file', path: '/root/src/components/Input.tsx', level: 3 }
]
}
]
},
{
name: 'docs',
type: 'directory',
path: '/root/docs',
level: 1,
children: [
{ name: 'README.md', type: 'file', path: '/root/docs/README.md', level: 2 }
]
},
{ name: 'package.json', type: 'file', path: '/root/package.json', level: 1 }
]
};
// UIAanmaken van elementen
const container = document.createElement('div');
document.body.appendChild(container);
const title = document.createElement('h3');
title.textContent = 'Weergave mappenhiërarchie';
container.appendChild(title);
const output = document.createElement('pre');
output.style.padding = '10px';
output.style.backgroundColor = '#f5f5f5';
output.style.fontFamily = 'monospace';
output.style.fontSize = '14px';
container.appendChild(output);
const stats = document.createElement('div');
stats.style.marginTop = '10px';
stats.style.padding = '10px';
stats.style.backgroundColor = '#e3f2fd';
container.appendChild(stats);
let fileCount = 0;
let dirCount = 0;
// Recursief uitgebreide mappenstructuur
of(fileSystem).pipe(
expand(item => {
if (item.type === 'directory' && item.children && item.children.length > 0) {
return from(
item.children.map(child => ({
...child,
level: item.level + 1
}))
);
}
return EMPTY;
}),
tap(item => {
if (item.type === 'file') {
fileCount++;
} else {
dirCount++;
}
})
).subscribe({
next: item => {
const indent = ' '.repeat(item.level);
const icon = item.type === 'directory' ? '📁' : '📄';
output.textContent += `${indent}${icon} ${item.name}\n`;
},
complete: () => {
stats.textContent = `Aantal mappen: ${dirCount}, Aantal bestanden: ${fileCount}`;
}
});📋 Type veilig gebruik.
Dit is een voorbeeld van een typeveilige implementatie in TypeScript die gebruik maakt van generics.
import { Observable, of, from, EMPTY } from 'rxjs';
import { expand, filter, take, defaultIfEmpty, reduce } from 'rxjs';
interface Node<T> {
value: T;
children?: Node<T>[];
}
class TreeTraversal<T> {
/**
* Doorloopt de boomstructuur met zoeken eerst in de breedte
*/
traverseBFS(root: Node<T>): Observable<Node<T>> {
return of(root).pipe(
expand(node =>
node.children && node.children.length > 0
? from(node.children)
: EMPTY
)
);
}
/**
* Zoek naar het eerste knooppunt dat aan de criteria voldoet
*/
findNode(
root: Node<T>,
predicate: (value: T) => boolean
): Observable<Node<T> | undefined> {
return this.traverseBFS(root).pipe(
filter(node => predicate(node.value)),
take(1),
defaultIfEmpty(undefined as Node<T> | undefined)
);
}
/**
* Telt het aantal knooppunten in de boomstructuur
*/
countNodes(root: Node<T>): Observable<number> {
return this.traverseBFS(root).pipe(
reduce((count) => count + 1, 0)
);
}
/**
* Zoekt alle knooppunten met een specifieke waarde
*/
findAllNodes(
root: Node<T>,
predicate: (value: T) => boolean
): Observable<Node<T>[]> {
return this.traverseBFS(root).pipe(
filter(node => predicate(node.value)),
reduce((acc, node) => [...acc, node], [] as Node<T>[])
);
}
}
// Gebruiksvoorbeeld
const tree: Node<string> = {
value: 'A',
children: [
{
value: 'B',
children: [
{ value: 'D' },
{ value: 'E' }
]
},
{
value: 'C',
children: [
{ value: 'F' }
]
}
]
};
const traversal = new TreeTraversal<string>();
// De hele boom doorlopen
traversal.traverseBFS(tree).subscribe(node => {
console.log(`Bezoekt: ${node.value}`);
});
// Uitvoer: Bezoekt: A, Bezoekt: B, Bezoekt: C, Bezoekt: D, Bezoekt: E, Bezoekt: F
// Zoekt naar een specifiek knooppunt
traversal.findNode(tree, value => value === 'D').subscribe(node => {
console.log(`Gevonden knooppunten: ${node?.value}`);
});
// Uitvoer: Gevonden knooppunten: D
// Aantal knooppunten tellen
traversal.countNodes(tree).subscribe(count => {
console.log(`Aantal knooppunten in de boom: ${count}`);
});
// Uitvoer: Aantal knooppunten in de boom: 6
// Krijg alle knooppunten die aan de criteria voldoen
traversal.findAllNodes(tree, value => value.length === 1).subscribe(nodes => {
console.log(`Knooppunt met één teken: ${nodes.map(n => n.value).join(', ')}`);
});
// Uitvoer: Knooppunt met één teken: A, B, C, D, E, F🎯 Gecombineerd met scheduler
expand werkt standaard synchroon, maar kan asynchroon worden bestuurd met een scheduler.
import { of, asyncScheduler } from 'rxjs';
import { expand, take } from 'rxjs';
// Synchroon (standaard)
console.log('Synchroon (standaard)expandBegint met');
of(1).pipe(
expand(x => of(x * 2)),
take(5)
).subscribe(x => console.log('Synchroon:', x));
console.log('Synchroon (standaard)expandEinde');
// Uitvoer:
// Synchroon (standaard)expandBegint met
// Synchroon: 1
// Synchroon: 2
// Synchroon: 4
// Synchroon: 8
// Synchroon: 16
// Synchroon (standaard)expandEinde
// Asynchroon (asyncSchedulerGebruiken)
console.log('AsynchroonexpandBegint met');
of(1, asyncScheduler).pipe(
expand(x => of(x * 2, asyncScheduler)),
take(5)
).subscribe(x => console.log('Asynchroon:', x));
console.log('AsynchroonexpandEinde');
// Uitvoer:
// AsynchroonexpandBegint met
// AsynchroonexpandEinde
// Asynchroon: 1
// Asynchroon: 2
// Asynchroon: 4
// Asynchroon: 8
// Asynchroon: 16uitklappen_16___
Bij het verwerken van grote hoeveelheden gegevens kan de
asyncSchedulergebruikt worden om de UI responsief te houden zonder de hoofd thread te blokkeren. Voor meer informatie, zie Scheduler types en hun gebruik.
Voorbeeld van recursieve berekening
Fibonacci-reeks.
import { of } from 'rxjs';
import { expand, take } from 'rxjs';
// 2Recursief proces van verdubbelen
of(1).pipe(
expand(x => of(x * 2)),
take(5) // Preventie van oneindige lus
).subscribe(console.log);
// Uitvoer: 1, 2, 4, 8, 16Factoriale berekeningen
⚠️ Veelgemaakte fouten
De meest voorkomende fout met
uitbreidenis om te vergeten een exitvoorwaarde in te stellen en in een oneindige lus** terecht te komen.
Fout: geen afsluitvoorwaarde.
Positief: met afsluitvoorwaarde
import { of } from 'rxjs';
import { expand, take } from 'rxjs';
// 2Recursief proces van verdubbelen
of(1).pipe(
expand(x => of(x * 2)),
take(5) // Preventie van oneindige lus
).subscribe(console.log);
// Uitvoer: 1, 2, 4, 8, 16Recursieve processen moeten altijd de exitconditie expliciet maken en oneindige lussen voorkomen door
take,takeWhileofEMPTYterug te geven, afhankelijk van de conditie.
Samenvatting
Wanneer expand moet worden gebruikt.
- ✅ Wanneer je een boomstructuur of grafiek recursief wilt doorlopen.
- ✅ Als u alle gegevens met API-paginering wilt ophalen
- ✅ Als je recursieve berekeningen wilt uitvoeren (Fibonacci, factorial, etc.)
- ✅ Als je directorystructuren of bestandssystemen wilt doorkruisen
- ✅ Als je organigrammen of hiërarchische gegevens wilt onderzoeken
Wanneer je mergeMap moet gebruiken.
- ✅ Als het voldoende is om elke waarde slechts één keer te converteren
- ✅ Normale asynchrone transformaties die geen recursieve verwerking vereisen
Opmerkingen.
- ⚠️ Stel altijd een afsluitvoorwaarde (om oneindige lussen te voorkomen)
- ⚠️ Wees voorzichtig met geheugengebruik (bij het extraheren van grote hoeveelheden gegevens)
- ⚠️ Omdat het synchroon werkt, overweeg het gebruik van
asyncSchedulervoor grote hoeveelheden gegevens. - ⚠️ Omdat debuggen moeilijk is, is het een goed idee om tussentijdse toestanden uit te loggen met
tap.
Volgende stappen.
- mergeMap - leer de gebruikelijke asynchrone transformaties.
- switchMap - leer de conversie om over te schakelen naar het nieuwste proces.
- concatMap - leer transformaties die sequentieel worden uitgevoerd.
- Scheduler-types en hun gebruik - leer hoe u expand en schedulers kunt combineren.
- Praktische voorbeelden van conversie operatoren - leer hoe je expand en schedulers kunt combineren /praktische-gebruiksgevallen)** - leer over echte gebruiksgevallen