Skip to content

Commit 1387e54

Browse files
committed
backporting the root implementation with memoize feature
1 parent b10891c commit 1387e54

19 files changed

+301
-168
lines changed

USAGE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ The `Watcher` constructor can be passed 3 different options:
111111

112112
* `time` - The time threshold
113113
* `ratio` - The ratio threshold
114-
* `rootMargin` - The [rootMargin](https://wicg.github.io/IntersectionObserver/#dom-intersectionobserverinit-rootmargin) in object form.
114+
* `rootMargin` - The [rootMargin](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#Intersection_observer_options) in object form.
115+
* `root` - The [root](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#Intersection_observer_options) element with respect to which we want to watch the target. By default it is window.
115116

116117
## Utility API
117118

src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,11 @@ import {
4646
Frame
4747
} from './metal/index';
4848

49+
4950
import w from './metal/window-proxy';
5051

52+
import { validateState } from './metal/window-proxy';
53+
5154
export {
5255
on,
5356
off,
@@ -58,7 +61,8 @@ export {
5861
SpanielTrackedElement,
5962
setGlobalEngine,
6063
getGlobalEngine,
61-
w as __w__
64+
w as __w__,
65+
validateState
6266
};
6367

6468
export function queryElement(el: Element, callback: (clientRect: ClientRect, frame: Frame) => void) {

src/interfaces.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export interface SpanielObserverInit {
2323
root?: SpanielTrackedElement;
2424
rootMargin?: DOMString | DOMMargin; // default: 0px
2525
threshold?: SpanielThreshold[]; // default: 0
26+
ALLOW_CACHED_SCHEDULER?: boolean;
2627
}
2728

2829
export interface SpanielRecord {
@@ -83,4 +84,5 @@ export interface IntersectionObserverInit {
8384
root?: SpanielTrackedElement;
8485
rootMargin?: DOMString; // default: 0px
8586
threshold?: number | number[]; // default: 0
87+
ALLOW_CACHED_SCHEDULER?: boolean;
8688
}

src/intersection-observer.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,14 +132,14 @@ export class SpanielIntersectionObserver implements IntersectionObserver {
132132
this.id = generateToken();
133133
options.threshold = options.threshold || 0;
134134
this.rootMarginObj = rootMarginToDOMMargin(options.rootMargin || '0px');
135-
135+
this.root = options.root;
136136
if (Array.isArray(options.threshold)) {
137137
this.thresholds = <Array<number>>options.threshold;
138138
} else {
139139
this.thresholds = [<number>options.threshold];
140140
}
141141

142-
this.scheduler = new ElementScheduler();
142+
this.scheduler = new ElementScheduler(null, this.root, options.ALLOW_CACHED_SCHEDULER);
143143
}
144144
};
145145

@@ -182,8 +182,8 @@ export class IntersectionObserverEntry implements IntersectionObserverEntryInit
182182
export function generateEntry(frame: Frame, clientRect: DOMRectReadOnly, el: Element, rootMargin: DOMMargin): IntersectionObserverEntry {
183183
let { top, bottom, left, right } = clientRect;
184184
let rootBounds: ClientRect = {
185-
left: rootMargin.left,
186-
top: rootMargin.top,
185+
left: frame.left + rootMargin.left,
186+
top: frame.top + rootMargin.top,
187187
bottom: rootMargin.bottom,
188188
right: rootMargin.right,
189189
width: frame.width - (rootMargin.right + rootMargin.left),

src/metal/interfaces.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,30 @@ export interface FrameInterface {
5252
scrollLeft: number;
5353
width: number;
5454
height: number;
55+
x: number;
56+
y: number;
57+
top: number;
58+
left: number;
5559
}
5660

5761
export interface MetaInterface {
62+
scrollTop: number;
63+
scrollLeft: number;
5864
width: number;
5965
height: number;
60-
scrollLeft: number;
61-
scrollTop: number;
66+
x: number;
67+
y: number;
68+
top: number;
69+
left: number;
6270
}
6371

64-
export interface OnWindowIsDirtyInterface {
65-
fn: any;
66-
scope: any;
67-
id: string;
72+
export interface SpanielClientRectInterface {
73+
width: number;
74+
height: number;
75+
x: number;
76+
y: number;
77+
bottom: number;
78+
top: number;
79+
left: number;
80+
right: number;
6881
}

src/metal/scheduler.ts

Lines changed: 76 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import {
1414
SchedulerInterface,
1515
ElementSchedulerInterface,
1616
FrameInterface,
17-
QueueInterface
17+
QueueInterface,
18+
MetaInterface,
19+
SpanielClientRectInterface
1820
} from './interfaces';
1921
import W from './window-proxy';
2022

@@ -29,45 +31,88 @@ const TOKEN_SEED = 'xxxx'.replace(/[xy]/g, function(c) {
2931
});
3032
let tokenCounter = 0;
3133

32-
function generateRandomToken() {
33-
return Math.floor(Math.random() * (9999999 - 0o0)).toString(16);
34-
}
35-
3634
export class Frame implements FrameInterface {
3735
constructor(
3836
public timestamp: number,
3937
public scrollTop: number,
4038
public scrollLeft: number,
4139
public width: number,
42-
public height: number
40+
public height: number,
41+
public x: number,
42+
public y: number,
43+
public top: number,
44+
public left: number
4345
) {}
44-
static generate(): Frame {
46+
static generate(root: Element | Window = window): Frame {
47+
const rootMeta = this.revalidateRootMeta(root);
4548
return new Frame(
4649
Date.now(),
47-
W.meta.scrollTop,
48-
W.meta.scrollLeft,
49-
W.meta.width,
50-
W.meta.height
50+
rootMeta.scrollTop,
51+
rootMeta.scrollLeft,
52+
rootMeta.width,
53+
rootMeta.height,
54+
rootMeta.x,
55+
rootMeta.y,
56+
rootMeta.top,
57+
rootMeta.left
5158
);
5259
}
60+
static revalidateRootMeta(root: any = window): MetaInterface {
61+
let _rootMeta: MetaInterface = {
62+
width: 0,
63+
height: 0,
64+
scrollTop: 0,
65+
scrollLeft: 0,
66+
x: 0,
67+
y: 0,
68+
top: 0,
69+
left: 0
70+
};
71+
72+
// if root is dirty update the cached values
73+
if (W.isDirty) { W.updateMeta(); }
74+
75+
if (root === window) {
76+
_rootMeta.height = W.meta.height;
77+
_rootMeta.width = W.meta.width;
78+
_rootMeta.scrollLeft = W.meta.scrollLeft;
79+
_rootMeta.scrollTop = W.meta.scrollTop;
80+
}else if (root) {
81+
let _clientRect = getBoundingClientRect(root);
82+
_rootMeta.scrollTop = root.scrollTop;
83+
_rootMeta.scrollLeft = root.scrollLeft;
84+
_rootMeta.width = _clientRect.width;
85+
_rootMeta.height = _clientRect.height;
86+
_rootMeta.x = _clientRect.x;
87+
_rootMeta.y = _clientRect.y;
88+
_rootMeta.top = _clientRect.top;
89+
_rootMeta.left = _clientRect.left;
90+
}
91+
92+
return _rootMeta;
93+
}
5394
}
5495

5596
export function generateToken() {
5697
return tokenCounter++ + TOKEN_SEED;
5798
}
5899

59100
export abstract class BaseScheduler {
101+
protected root: Element | Window;
60102
protected engine: EngineInterface;
61103
protected queue: QueueInterface;
62104
protected isTicking: Boolean = false;
63105
protected toRemove: Array<string| Element | Function> = [];
106+
protected id?: string;
64107

65-
constructor(customEngine?: EngineInterface) {
108+
constructor(customEngine?: EngineInterface, root: Element | Window = window) {
66109
if (customEngine) {
67110
this.engine = customEngine;
68111
} else {
69112
this.engine = getGlobalEngine();
70113
}
114+
115+
this.root = root;
71116
}
72117
protected abstract applyQueue(frame: Frame): void;
73118

@@ -81,8 +126,7 @@ export abstract class BaseScheduler {
81126
}
82127
this.toRemove = [];
83128
}
84-
85-
this.applyQueue(Frame.generate());
129+
this.applyQueue(Frame.generate(this.root));
86130
this.engine.scheduleRead(this.tick.bind(this));
87131
}
88132
}
@@ -97,7 +141,7 @@ export abstract class BaseScheduler {
97141
let frame: Frame = null;
98142
this.engine.scheduleRead(() => {
99143
clientRect = getBoundingClientRect(el);
100-
frame = Frame.generate();
144+
frame = Frame.generate(this.root);
101145
});
102146
this.engine.scheduleWork(() => {
103147
callback(clientRect, frame);
@@ -108,7 +152,6 @@ export abstract class BaseScheduler {
108152
}
109153
unwatchAll() {
110154
this.queue.clear();
111-
W.__destroy__();
112155
}
113156
startTicking() {
114157
if (!this.isTicking) {
@@ -140,7 +183,7 @@ export class Scheduler extends BaseScheduler implements SchedulerInterface {
140183
export class PredicatedScheduler extends Scheduler implements SchedulerInterface {
141184
predicate: (frame: Frame) => Boolean;
142185
constructor(predicate: (frame: Frame) => Boolean) {
143-
super(null);
186+
super(null, window);
144187
this.predicate = predicate;
145188
}
146189
applyQueue(frame: Frame) {
@@ -152,13 +195,17 @@ export class PredicatedScheduler extends Scheduler implements SchedulerInterface
152195

153196
export class ElementScheduler extends BaseScheduler implements ElementSchedulerInterface {
154197
protected queue: DOMQueue;
155-
protected isDirty: boolean = false;
156-
protected id: string = '';
198+
protected lastVersion: number = W.version;
199+
protected ALLOW_CACHED_SCHEDULER: boolean;
157200

158-
constructor(customEngine?: EngineInterface) {
159-
super(customEngine);
201+
constructor(customEngine?: EngineInterface, root?: Element | Window, ALLOW_CACHED_SCHEDULER: boolean = false) {
202+
super(customEngine, root);
160203
this.queue = new DOMQueue();
161-
this.id = generateRandomToken();
204+
this.ALLOW_CACHED_SCHEDULER = ALLOW_CACHED_SCHEDULER;
205+
}
206+
207+
get isDirty(): boolean {
208+
return W.version !== this.lastVersion;
162209
}
163210

164211
applyQueue(frame: Frame) {
@@ -169,14 +216,19 @@ export class ElementScheduler extends BaseScheduler implements ElementSchedulerI
169216
clientRect = this.queue.items[i].clientRect = getBoundingClientRect(el);
170217
}
171218

219+
// FLAG WHICH WILL EVENTUALLY BE REMOVED
220+
// THE EXPERIMENTAL FLAG DEFAULTS TO OFF
221+
if (!this.ALLOW_CACHED_SCHEDULER) {
222+
clientRect = this.queue.items[i].clientRect = getBoundingClientRect(el);
223+
}
224+
172225
callback(frame, id, clientRect);
173226
}
174227

175-
this.isDirty = false;
228+
this.lastVersion = W.version;
176229
}
177230

178231
watch(el: Element, callback: (frame: FrameInterface, id: string, clientRect?: ClientRect | null) => void, id?: string): string {
179-
this.initWindowIsDirtyListeners();
180232
this.startTicking();
181233
id = id || generateToken();
182234
let clientRect = null;
@@ -189,14 +241,6 @@ export class ElementScheduler extends BaseScheduler implements ElementSchedulerI
189241
});
190242
return id;
191243
}
192-
193-
initWindowIsDirtyListeners() {
194-
W.onWindowIsDirtyListeners.push({ fn: this.windowIsDirtyHandler, scope: this, id: this.id });
195-
}
196-
197-
windowIsDirtyHandler() {
198-
this.isDirty = true;
199-
}
200244
}
201245

202246
let globalScheduler: Scheduler = null;

0 commit comments

Comments
 (0)