-
Notifications
You must be signed in to change notification settings - Fork 33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
backporting the root implementation with memoize feature #80
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
@@ -111,7 +111,8 @@ The `Watcher` constructor can be passed 3 different options: | |||
|
|||
* `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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why get rid of the link
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/metal/scheduler.ts
Outdated
static generate(): Frame { | ||
static generate(root?: Element): Frame { | ||
if (root && document.documentElement.contains(root)) { | ||
let rootClientRect = getBoundingClientRect(root); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be a const
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Confirmed
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does consolidating into one throttle function buy us anything if we are still switching on each case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nothing major. One less function declaration in a succinct easier to read package.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also the second any of root
appears to be never used, we can likely drop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
if (customEngine) { | ||
this.engine = customEngine; | ||
} else { | ||
this.engine = getGlobalEngine(); | ||
} | ||
|
||
if (root) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
@@ -43,19 +47,35 @@ let W: WindowProxy = { | |||
getScrollLeft: nop, | |||
getHeight: nop, | |||
getWidth: nop, | |||
rootsList: [], | |||
onWindowIsDirtyListeners: [], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This data-structure seems better suited to be a Map
then an Array
.
src/metal/window-proxy.ts
Outdated
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what disconnects these listeners from custom roots?
src/metal/window-proxy.ts
Outdated
onWindowIsDirtyListeners: [], | ||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
meta: { | ||
width: 0, | ||
height: 0, | ||
scrollTop: 0, | ||
scrollLeft: 0 | ||
}, | ||
__destroy__() { | ||
disconnectIsDirtyListener(id: string) { | ||
this.onWindowIsDirtyListeners = this.onWindowIsDirtyListeners.filter((obj: any) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if this becomes a map, this would become: this.onWindowIsDirtyListeners.delete(id)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what unbinds this scroll event
src/native-watcher.ts
Outdated
@@ -88,9 +91,17 @@ export class Watcher { | |||
}); | |||
} | |||
|
|||
if (root) { | |||
if (!W.rootsList.includes(root)) { | |||
W.rootsList.push(root); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what removes from this list
src/utils.ts
Outdated
try { | ||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh IE, you so cray cray
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would it be safe to drop e.description.split(' ')[0] === 'Unspecified' &&
?
src/watcher.ts
Outdated
@@ -87,10 +90,18 @@ export class Watcher { | |||
}); | |||
} | |||
|
|||
if (root) { | |||
if (!W.rootsList.includes(root)) { | |||
W.rootsList.push(root); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what removes from this list?
src/watcher.ts
Outdated
if (root) { | ||
if (!W.rootsList.includes(root)) { | ||
W.rootsList.push(root); | ||
W.bindRootScrollEvent(root); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what unbinds this event?
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why have an isDirty + version + lastVersion?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isDirty is doing the validation check from version to lastVersion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
last
that pertain to thelastVersion
ielastWidth
,
lastTop
etc.
ALLOW_CACHED_SCHEDULER: true | ||
}); | ||
|
||
root.addEventListener('scroll', () => spaniel.invalidate(), false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this doesn't look like it is removed if the observer is disposed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I realize this is a test, but is this the pattern that is encouraged?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
root
implementation withinIntersection-Observer
.