Skip to content

Commit 7a90374

Browse files
committed
feat(observers): allow for mutation observer options to be customized in content observer
Currently we always use the same set of `MutationObserver` options to monitor an element, however if the consumer knows what they want to monitor, they could gain a performance benefit from configuring the observer differently. These changes rework the `ContentObserver` to allow it to handle multiple observers per element that are cached based on their options.
1 parent 49527e5 commit 7a90374

File tree

3 files changed

+122
-34
lines changed

3 files changed

+122
-34
lines changed

src/cdk/observers/observe-content.spec.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,8 @@ describe('ContentObserver injectable', () => {
174174
const fixture = TestBed.createComponent(UnobservedComponentWithTextContent);
175175
fixture.detectChanges();
176176

177-
const sub1 = contentObserver.observe(fixture.componentInstance.contentEl)
178-
.subscribe(() => spy());
179-
contentObserver.observe(fixture.componentInstance.contentEl)
180-
.subscribe(() => spy());
177+
const sub1 = contentObserver.observe(fixture.componentInstance.contentEl).subscribe(spy);
178+
const sub2 = contentObserver.observe(fixture.componentInstance.contentEl).subscribe(spy);
181179

182180
expect(mof.create).toHaveBeenCalledTimes(1);
183181

@@ -192,7 +190,40 @@ describe('ContentObserver injectable', () => {
192190
invokeCallbacks();
193191

194192
expect(spy).toHaveBeenCalledTimes(1);
193+
sub2.unsubscribe();
195194
})));
195+
196+
197+
it('should create multiple observers when observing with different options',
198+
fakeAsync(inject([MutationObserverFactory], (mof: MutationObserverFactory) => {
199+
const spy = jasmine.createSpy('content observer');
200+
spyOn(mof, 'create').and.callThrough();
201+
const fixture = TestBed.createComponent(UnobservedComponentWithTextContent);
202+
fixture.detectChanges();
203+
204+
const sub1 = contentObserver.observe(fixture.componentInstance.contentEl, {
205+
characterData: true
206+
}).subscribe(spy);
207+
const sub2 = contentObserver.observe(fixture.componentInstance.contentEl, {
208+
childList: true
209+
}).subscribe(spy);
210+
211+
expect(mof.create).toHaveBeenCalledTimes(2);
212+
213+
fixture.componentInstance.text = 'text';
214+
invokeCallbacks();
215+
216+
expect(spy).toHaveBeenCalledTimes(2);
217+
218+
fixture.componentInstance.text = 'text text';
219+
invokeCallbacks();
220+
221+
expect(spy).toHaveBeenCalledTimes(4);
222+
223+
sub1.unsubscribe();
224+
sub2.unsubscribe();
225+
})));
226+
196227
});
197228
});
198229

src/cdk/observers/observe-content.ts

Lines changed: 85 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -38,40 +38,49 @@ export class MutationObserverFactory {
3838
@Injectable({providedIn: 'root'})
3939
export class ContentObserver implements OnDestroy {
4040
/** Keeps track of the existing MutationObservers so they can be reused. */
41-
private _observedElements = new Map<Element, {
41+
private _observedElements = new Map<Element, Map<string, {
4242
observer: MutationObserver | null,
4343
stream: Subject<MutationRecord[]>,
4444
count: number
45-
}>();
45+
}>>();
4646

4747
constructor(private _mutationObserverFactory: MutationObserverFactory) {}
4848

4949
ngOnDestroy() {
50-
this._observedElements.forEach((_, element) => this._cleanupObserver(element));
50+
this._observedElements.forEach((cache, element) => {
51+
cache.forEach((_, key) => this._cleanupObserver(element, key));
52+
});
5153
}
5254

5355
/**
5456
* Observe content changes on an element.
5557
* @param element The element to observe for content changes.
58+
* @param options Options that can be used to configure what is being observed,
5659
*/
57-
observe(element: Element): Observable<MutationRecord[]>;
60+
observe(element: Element, options?: MutationObserverInit): Observable<MutationRecord[]>;
5861

5962
/**
6063
* Observe content changes on an element.
6164
* @param element The element to observe for content changes.
65+
* @param options Options that can be used to configure what is being observed,
6266
*/
63-
observe(element: ElementRef<Element>): Observable<MutationRecord[]>;
67+
observe(element: ElementRef<Element>, options?: MutationObserverInit):
68+
Observable<MutationRecord[]>;
6469

65-
observe(elementOrRef: Element | ElementRef<Element>): Observable<MutationRecord[]> {
70+
observe(elementOrRef: Element | ElementRef<Element>, options: MutationObserverInit = {
71+
characterData: true,
72+
childList: true,
73+
subtree: true
74+
}): Observable<MutationRecord[]> {
6675
const element = coerceElement(elementOrRef);
6776

6877
return new Observable((observer: Observer<MutationRecord[]>) => {
69-
const stream = this._observeElement(element);
78+
const stream = this._observeElement(element, options);
7079
const subscription = stream.subscribe(observer);
7180

7281
return () => {
7382
subscription.unsubscribe();
74-
this._unobserveElement(element);
83+
this._unobserveElement(element, options);
7584
};
7685
});
7786
}
@@ -80,47 +89,95 @@ export class ContentObserver implements OnDestroy {
8089
* Observes the given element by using the existing MutationObserver if available, or creating a
8190
* new one if not.
8291
*/
83-
private _observeElement(element: Element): Subject<MutationRecord[]> {
84-
if (!this._observedElements.has(element)) {
92+
private _observeElement(element: Element, options: MutationObserverInit):
93+
Subject<MutationRecord[]> {
94+
95+
const observedElements = this._observedElements;
96+
const cacheKey = this._getCacheKey(options);
97+
let elementEntry = observedElements.get(element);
98+
99+
if (!elementEntry) {
100+
elementEntry = new Map();
101+
observedElements.set(element, elementEntry);
102+
}
103+
104+
const cachedConfig = elementEntry.get(cacheKey);
105+
106+
if (cachedConfig) {
107+
cachedConfig.count++;
108+
return cachedConfig.stream;
109+
} else {
85110
const stream = new Subject<MutationRecord[]>();
86111
const observer = this._mutationObserverFactory.create(mutations => stream.next(mutations));
112+
87113
if (observer) {
88-
observer.observe(element, {
89-
characterData: true,
90-
childList: true,
91-
subtree: true
92-
});
114+
observer.observe(element, options);
93115
}
94-
this._observedElements.set(element, {observer, stream, count: 1});
95-
} else {
96-
this._observedElements.get(element)!.count++;
116+
117+
elementEntry.set(cacheKey, {observer, stream, count: 1});
118+
return stream;
97119
}
98-
return this._observedElements.get(element)!.stream;
99120
}
100121

101122
/**
102123
* Un-observes the given element and cleans up the underlying MutationObserver if nobody else is
103124
* observing this element.
104125
*/
105-
private _unobserveElement(element: Element) {
106-
if (this._observedElements.has(element)) {
107-
this._observedElements.get(element)!.count--;
108-
if (!this._observedElements.get(element)!.count) {
109-
this._cleanupObserver(element);
126+
private _unobserveElement(element: Element, options: MutationObserverInit) {
127+
const cacheKey = this._getCacheKey(options);
128+
const cachedConfig = this._getConfig(element, cacheKey);
129+
130+
if (cachedConfig) {
131+
cachedConfig.count--;
132+
133+
if (cachedConfig.count < 1) {
134+
this._cleanupObserver(element, cacheKey);
110135
}
111136
}
112137
}
113138

114139
/** Clean up the underlying MutationObserver for the specified element. */
115-
private _cleanupObserver(element: Element) {
116-
if (this._observedElements.has(element)) {
117-
const {observer, stream} = this._observedElements.get(element)!;
140+
private _cleanupObserver(element: Element, cacheKey: string) {
141+
const cachedConfig = this._getConfig(element, cacheKey);
142+
143+
if (cachedConfig) {
144+
const {observer, stream} = cachedConfig;
145+
118146
if (observer) {
119147
observer.disconnect();
120148
}
149+
121150
stream.complete();
122-
this._observedElements.delete(element);
151+
this._observedElements.get(element)!.delete(cacheKey);
152+
153+
if (this._observedElements.get(element)!.size < 1) {
154+
this._observedElements.delete(element);
155+
}
156+
}
157+
}
158+
159+
/** Gets the cached config for an element, based on a cache key. */
160+
private _getConfig(element: Element, cacheKey: string) {
161+
const elementEntry = this._observedElements.get(element);
162+
163+
if (elementEntry) {
164+
return elementEntry.get(cacheKey);
123165
}
166+
167+
return undefined;
168+
}
169+
170+
/** Generates a key for the element cache from a MutationObserver configuration object. */
171+
private _getCacheKey(options: MutationObserverInit): string {
172+
return [
173+
options.attributeFilter ? options.attributeFilter.join(',') : '',
174+
!!options.attributeOldValue,
175+
!!options.attributes,
176+
!!options.characterData,
177+
!!options.characterDataOldValue,
178+
!!options.childList,
179+
!!options.subtree
180+
].join('|');
124181
}
125182
}
126183

tools/public_api_guard/cdk/observers.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ export declare class CdkObserveContent implements AfterContentInit, OnDestroy {
1414
export declare class ContentObserver implements OnDestroy {
1515
constructor(_mutationObserverFactory: MutationObserverFactory);
1616
ngOnDestroy(): void;
17-
observe(element: Element): Observable<MutationRecord[]>;
18-
observe(element: ElementRef<Element>): Observable<MutationRecord[]>;
17+
observe(element: Element, options?: MutationObserverInit): Observable<MutationRecord[]>;
18+
observe(element: ElementRef<Element>, options?: MutationObserverInit): Observable<MutationRecord[]>;
1919
static ɵfac: i0.ɵɵFactoryDef<ContentObserver>;
2020
static ɵprov: i0.ɵɵInjectableDef<ContentObserver>;
2121
}

0 commit comments

Comments
 (0)