backporting the root implementation with memoize feature#80
backporting the root implementation with memoize feature#80lynchbomb merged 1 commit intov2-releasefrom
Conversation
asakusuma
left a comment
There was a problem hiding this comment.
I’ll take another pass later. A few questions:
- Does this change handle nested roots?
- I assume this handles the scenario where some other code captures the window scroll event?
USAGE.md
Outdated
| * `time` - The time threshold | ||
| * `ratio` - The ratio threshold | ||
| * `rootMargin` - The [rootMargin](https://wicg.github.io/IntersectionObserver/#dom-intersectionobserverinit-rootmargin) in object form. | ||
| * `rootMargin` - The rootMargin in object form. |
There was a problem hiding this comment.
It's 404-ing. FWIW rootMargin is as well. Just need to find the new relative links for these.
There was a problem hiding this comment.
src/metal/scheduler.ts
Outdated
| static generate(): Frame { | ||
| static generate(root?: Element): Frame { | ||
| if (root && document.documentElement.contains(root)) { | ||
| let rootClientRect = getBoundingClientRect(root); |
src/metal/scheduler.ts
Outdated
| ) {} | ||
| static generate(): Frame { | ||
| static generate(root?: Element): Frame { | ||
| if (root && document.documentElement.contains(root)) { |
There was a problem hiding this comment.
In the past, we have used the window proxy for any direct touching of the document or the window, so that things don’t break in node land. Is this a safe direct document reference?
There was a problem hiding this comment.
Nice catch. Besides the private conditionals within the window proxy for hasDomSetup and hasDOM we aren't currently (to my knowledge) checking for DOM before leveraging the document. We should definitely think about exporting a document proxy or even just a document interface to check for DOM etc.
src/metal/window-proxy.ts
Outdated
| windowSetScrollMeta(); | ||
| }, throttleDelay); | ||
| // Only invalidate window isDirty on scroll and resize | ||
| function eventThrottle(eventType?: string, root?: Element) { |
There was a problem hiding this comment.
Does consolidating into one throttle function buy us anything if we are still switching on each case?
There was a problem hiding this comment.
Nothing major. One less function declaration in a succinct easier to read package.
There was a problem hiding this comment.
I would strongly recommend using 2 discrete methods.
Rationale (for 2 functions)
- more concise (less bytes)
- less prone to error (if an invalid event type is used, we get a useful error)
- remove unneeded wrapper function
- we should defer machinery for extensibility until not having that machinery is causes grief.
There was a problem hiding this comment.
also the second any of root appears to be never used, we can likely drop.
There was a problem hiding this comment.
If a throttle utility is to be used, we should do so to avoid extra bytes. I believe something like the following could be used:
function throttle(cb) {
let cookie;
return () => {
clearTimeout(cookie);
cookie = setTimeout(cb, throttleDelay)
// anything else, maybe the dirty check thing.
}
}
addEventListener('resize', throttle(windowSetDimensionsMeta))
addEventListener('scroll', throttle(windowSetScrollMeta));
It does not. I think at first pass we should focus on MVP. Especially since we are back-porting
Nice catch actually. It does not. I'll include a fallback for the window and root scroll events to leverage page offset and clientRect deltas respectively. Since we don't want to check clientRect per frame as the primary identifier for |
src/metal/scheduler.ts
Outdated
| this.engine = getGlobalEngine(); | ||
| } | ||
|
|
||
| if (root) { |
There was a problem hiding this comment.
I think we can drop this guard, setting root regardless will ensure the schedular will have the same number of slots between different instances. In addition, we do use this.root, and thus is may as well be set on at this level, rather then falling back down the prototype chain.
src/metal/window-proxy.ts
Outdated
| getHeight: nop, | ||
| getWidth: nop, | ||
| rootsList: [], | ||
| onWindowIsDirtyListeners: [], |
There was a problem hiding this comment.
This data-structure seems better suited to be a Map then an Array.
src/metal/window-proxy.ts
Outdated
| rootsList: [], | ||
| onWindowIsDirtyListeners: [], | ||
| rAF: hasRAF ? window.requestAnimationFrame.bind(window) : (callback: Function) => { callback(); }, | ||
| bindRootScrollEvent: (root: Element) => { root.addEventListener('scroll', () => W.callIsDirtyListeners(), false); }, |
There was a problem hiding this comment.
what disconnects these listeners from custom roots?
src/metal/window-proxy.ts
Outdated
| rAF: hasRAF ? window.requestAnimationFrame.bind(window) : (callback: Function) => { callback(); }, | ||
| bindRootScrollEvent: (root: Element) => { root.addEventListener('scroll', () => W.callIsDirtyListeners(), false); }, | ||
| callIsDirtyListeners() { | ||
| if (this.onWindowIsDirtyListeners.length <= 0) { return; } |
There was a problem hiding this comment.
This guard seems redundant, as forEach wont iterate if the list is empty. We should likely only add more code (for perf) if it we have seen a good reason to do so.
src/metal/window-proxy.ts
Outdated
| }, | ||
| __destroy__() { | ||
| disconnectIsDirtyListener(id: string) { | ||
| this.onWindowIsDirtyListeners = this.onWindowIsDirtyListeners.filter((obj: any) => { |
There was a problem hiding this comment.
if this becomes a map, this would become: this.onWindowIsDirtyListeners.delete(id)
There was a problem hiding this comment.
if we do the above VERSION + instance._lastVersion stuff, this can be removed entirely.
src/native-watcher.ts
Outdated
| if (root) { | ||
| if (!W.rootsList.includes(root)) { | ||
| W.rootsList.push(root); | ||
| W.bindRootScrollEvent(root); |
There was a problem hiding this comment.
what unbinds this scroll event
src/native-watcher.ts
Outdated
|
|
||
| if (root) { | ||
| if (!W.rootsList.includes(root)) { | ||
| W.rootsList.push(root); |
There was a problem hiding this comment.
what removes from this list
src/utils.ts
Outdated
| return element.getBoundingClientRect(); | ||
| return element.getBoundingClientRect() as SpanielClientRectInterface; | ||
| } catch (e) { | ||
| if (typeof e === 'object' && e !== null && e.description.split(' ')[0] === 'Unspecified' && (e.number & 0xFFFF) === 16389) { |
There was a problem hiding this comment.
would it be safe to drop e.description.split(' ')[0] === 'Unspecified' && ?
src/watcher.ts
Outdated
|
|
||
| if (root) { | ||
| if (!W.rootsList.includes(root)) { | ||
| W.rootsList.push(root); |
There was a problem hiding this comment.
what removes from this list?
src/watcher.ts
Outdated
| if (root) { | ||
| if (!W.rootsList.includes(root)) { | ||
| W.rootsList.push(root); | ||
| W.bindRootScrollEvent(root); |
src/metal/scheduler.ts
Outdated
| ) {} | ||
| static generate(): Frame { | ||
| static generate(root?: Element): Frame { | ||
| if (root && document.documentElement.contains(root)) { |
There was a problem hiding this comment.
This guard, and the guard contained with getBoundingClientRect may be redundant when used together. As if the getBoundingClientRect fails, the various unknown attributes become 0,0,0,0 already right?
|
@stefanpenner and I spoke at length offline yesterday, about a hypothetical alternative to the reviewed comments and PR implementation above. In-which the hypothesis will need to be tested. If proven to be a viable alternative. The release of these changes must be behind a minor release + flag. This includes but not limited to:
The above implementation and API change would also set us up for major simplifications. Which would be included within a future v4 major release. Such as:
|
d8b8c9b to
190bcc3
Compare
|
@stefanpenner FWIW here is the PR spike for |
|
I chatted with @krisselden offline regarding the review of this PR. The primary feedback was when a custom root element is being watched, we should bind a scroll event within Spaniel on that custom root element to trigger cached state validation (ie the first iteration of this PR). That the common case would always require a scroll event on the custom root; keeping the API as dead simple as possible. Additionally (4/19/18):
Additionally and outside the scope of this PR about the idea of Backburner implementing a measure queue, in-which would be highly leveragable with Spaniel. |
9959766 to
b49d3b2
Compare
992951c to
1387e54
Compare
src/metal/window-proxy.ts
Outdated
| } | ||
| }; | ||
|
|
||
| export function validateState() { |
There was a problem hiding this comment.
name is confusing, I expected invalidate() or incrementVersion() or makeStateDirty()
| version: number; | ||
| lastVersion: number; | ||
| updateMeta: Function; | ||
| isDirty: boolean; |
There was a problem hiding this comment.
why have an isDirty + version + lastVersion?
There was a problem hiding this comment.
isDirty is doing the validation check from version to lastVersion.
There was a problem hiding this comment.
For this PR I am not going to address this. However we will table this and include in Spaniel 4.0.0:
- Cached version state on the Window Proxy will be moved to have its own interface with a version.
- One flat object with the prefix
lastthat pertain to thelastVersionielastWidth,
lastTopetc.
| ALLOW_CACHED_SCHEDULER: true | ||
| }); | ||
|
|
||
| root.addEventListener('scroll', () => spaniel.invalidate(), false); |
There was a problem hiding this comment.
this doesn't look like it is removed if the observer is disposed
There was a problem hiding this comment.
I realize this is a test, but is this the pattern that is encouraged?
There was a problem hiding this comment.
Just as the consuming app will need to handle binding the event they are also responsible for removing it. In Ember-Spaniel for example, we will provide a clear API to do both (on/off) within Embers lifecycle hooks.
rootimplementation withinIntersection-Observer.