Skip to content

Commit

Permalink
Refactored element observables, removed memleaks
Browse files Browse the repository at this point in the history
  • Loading branch information
squidfunk committed Aug 22, 2020
1 parent 989b859 commit 8a6b89f
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 17 deletions.
5 changes: 2 additions & 3 deletions src/assets/javascripts/browser/element/focus/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
*/

import { Observable, fromEvent, merge } from "rxjs"
import { map, shareReplay, startWith } from "rxjs/operators"
import { map, startWith } from "rxjs/operators"

import { getActiveElement } from "../_"

Expand Down Expand Up @@ -62,7 +62,6 @@ export function watchElementFocus(
)
.pipe(
map(({ type }) => type === "focus"),
startWith(el === getActiveElement()),
shareReplay(1)
startWith(el === getActiveElement())
)
}
5 changes: 2 additions & 3 deletions src/assets/javascripts/browser/element/offset/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
*/

import { Observable, fromEvent, merge } from "rxjs"
import { map, shareReplay, startWith } from "rxjs/operators"
import { map, startWith } from "rxjs/operators"

/* ----------------------------------------------------------------------------
* Types
Expand Down Expand Up @@ -71,7 +71,6 @@ export function watchElementOffset(
)
.pipe(
map(() => getElementOffset(el)),
startWith(getElementOffset(el)),
shareReplay(1)
startWith(getElementOffset(el))
)
}
80 changes: 69 additions & 11 deletions src/assets/javascripts/browser/element/size/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,23 @@
*/

import ResizeObserver from "resize-observer-polyfill"
import { Observable, fromEventPattern } from "rxjs"
import { shareReplay, startWith } from "rxjs/operators"
import {
NEVER,
Observable,
Subject,
defer,
merge,
of
} from "rxjs"
import {
filter,
finalize,
map,
shareReplay,
startWith,
switchMap,
tap
} from "rxjs/operators"

/* ----------------------------------------------------------------------------
* Types
Expand All @@ -36,6 +51,40 @@ export interface ElementSize {
height: number /* Element height */
}

/* ----------------------------------------------------------------------------
* Data
* ------------------------------------------------------------------------- */

/**
* Resize observer entry subject
*/
const entry$ = new Subject<ResizeObserverEntry>()

/**
* Resize observer observable
*
* This observable will create a `ResizeObserver` on the first subscription
* and will automatically terminate it when there are no more subscribers.
* It's quite important to centralize observation in a single `ResizeObserver`,
* as the performance difference can be quite dramatic, as the link shows.
*
* @see https://bit.ly/3iIYfEm - Google Groups on performance
*/
const observer$ = defer(() => of(
new ResizeObserver(entries => {
for (const entry of entries)
entry$.next(entry)
})
))
.pipe(
switchMap(resize => merge(of(resize), NEVER)
.pipe(
finalize(() => resize.disconnect())
)
),
shareReplay({ bufferSize: 1, refCount: true })
)

/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
Expand All @@ -59,22 +108,31 @@ export function getElementSize(el: HTMLElement): ElementSize {
/**
* Watch element size
*
* This function returns an observable that will subscribe to a single internal
* instance of `ResizeObserver` upon subscription, and emit resize events until
* termination. Note that this function should not be called with the same
* element twice, as the first unsubscription will terminate observation.
*
* @param el - Element
*
* @return Element size observable
*/
export function watchElementSize(
el: HTMLElement
): Observable<ElementSize> {
return fromEventPattern<ElementSize>(next => {
new ResizeObserver(([{ contentRect }]) => next({
width: Math.round(contentRect.width),
height: Math.round(contentRect.height)
}))
.observe(el)
})
return observer$
.pipe(
startWith(getElementSize(el)),
shareReplay(1)
tap(observer => observer.observe(el)),
switchMap(observer => entry$
.pipe(
filter(({ target }) => target === el),
finalize(() => observer.unobserve(el)),
map(({ contentRect }) => ({
width: contentRect.width,
height: contentRect.height
}))
)
),
startWith(getElementSize(el))
)
}

0 comments on commit 8a6b89f

Please sign in to comment.