35
type CallbackConfigType = {|
36
scheduledCallback: FrameCallbackType,
37
timeoutTime: number,
38
- callbackId: number, // used for cancelling
38
+ next: CallbackConfigType | null, // creating a linked list
39
+ prev: CallbackConfigType | null, // creating a linked list
40
|};
41
41
-import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment';
42
+export type CallbackIdType = CallbackConfigType;
43
+
44
import requestAnimationFrameForReact from 'shared/requestAnimationFrameForReact';
45
+import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment';
46
47
const hasNativePerformanceNow =
48
typeof performance === 'object' && typeof performance.now === 'function';
58
};
59
}
60
58
-// TODO: There's no way to cancel, because Fiber doesn't atm.
61
let scheduleWork: (
62
callback: FrameCallbackType,
63
options?: {timeout: number},
62
-) => number;
63
-let cancelScheduledWork: (callbackId: number) => void;
64
+) => CallbackIdType;
65
+let cancelScheduledWork: (callbackId: CallbackIdType) => void;
66
67
if (!ExecutionEnvironment.canUseDOM) {
66
- let callbackIdCounter = 0;
67
- // Timeouts are objects in Node.
68
- // For consistency, we'll use numbers in the public API anyway.
69
- const timeoutIds: {[number]: TimeoutID} = {};
68
+ const timeoutIds = new Map();
69
70
scheduleWork = function(
71
callback: FrameCallbackType,
72
options?: {timeout: number},
74
- ): number {
75
- const callbackId = callbackIdCounter++;
73
+ ): CallbackIdType {
74
+ // keeping return type consistent
75
+ const callbackConfig = {
76
+ scheduledCallback: callback,
77
+ timeoutTime: 0,
78
+ next: null,
79
+ prev: null,
80
+ };
81
const timeoutId = setTimeout(() => {
82
callback({
83
timeRemaining() {
86
didTimeout: false,
87
});
88
});
84
- timeoutIds[callbackId] = timeoutId;
85
- return callbackId;
89
+ timeoutIds.set(callback, timeoutId);
90
+ return callbackConfig;
91
};
87
- cancelScheduledWork = function(callbackId: number) {
88
- const timeoutId = timeoutIds[callbackId];
89
- delete timeoutIds[callbackId];
92
+ cancelScheduledWork = function(callbackId: CallbackIdType) {
93
+ const callback = callbackId.scheduledCallback;
94
+ const timeoutId = timeoutIds.get(callback);
95
+ timeoutIds.delete(callbackId);
96
clearTimeout(timeoutId);
97
};
98
} else {
93
- // We keep callbacks in a queue.
94
- // Calling scheduleWork will push in a new callback at the end of the queue.
95
- // When we get idle time, callbacks are removed from the front of the queue
96
- // and called.
97
- const pendingCallbacks: Array<CallbackConfigType> = [];
98
-
99
- let callbackIdCounter = 0;
100
- const getCallbackId = function(): number {
101
- callbackIdCounter++;
102
- return callbackIdCounter;
103
- };
104
-
105
- // When a callback is scheduled, we register it by adding it's id to this
106
- // object.
107
- // If the user calls 'cancelScheduledWork' with the id of that callback, it will be
108
- // unregistered by removing the id from this object.
109
- // Then we skip calling any callback which is not registered.
110
- // This means cancelling is an O(1) time complexity instead of O(n).
111
- const registeredCallbackIds: {[number]: boolean} = {};
99
+ let headOfPendingCallbacksLinkedList: CallbackConfigType | null = null;
100
+ let tailOfPendingCallbacksLinkedList: CallbackConfigType | null = null;
101
102
// We track what the next soonest timeoutTime is, to be able to quickly tell
103
// if none of the scheduled callbacks have timed out.
121
},
122
};
123
135
- const safelyCallScheduledCallback = function(
136
- callback: FrameCallbackType,
137
- callbackId: number,
138
- ) {
139
- if (!registeredCallbackIds[callbackId]) {
140
- // ignore cancelled callbacks
141
- return;
142
- }
143
- try {
144
- callback(frameDeadlineObject);
145
- // Avoid using 'catch' to keep errors easy to debug
146
- } finally {
147
- // always clean up the callbackId, even if the callback throws
148
- delete registeredCallbackIds[callbackId];
149
- }
150
- };
151
-
124
/**
125
* Checks for timed out callbacks, runs them, and then checks again to see if
126
* any more have timed out.
127
* Keeps doing this until there are none which have currently timed out.
128
*/
129
const callTimedOutCallbacks = function() {
158
- if (pendingCallbacks.length === 0) {
130
+ if (headOfPendingCallbacksLinkedList === null) {
131
return;
132
}
133
148
149
// keep checking until we don't find any more timed out callbacks
150
frameDeadlineObject.didTimeout = true;
179
- for (let i = 0, len = pendingCallbacks.length; i < len; i++) {
180
- const currentCallbackConfig = pendingCallbacks[i];
151
+ let currentCallbackConfig = headOfPendingCallbacksLinkedList;
152
+ while (currentCallbackConfig !== null) {
153
const timeoutTime = currentCallbackConfig.timeoutTime;
154
if (timeoutTime !== -1 && timeoutTime <= currentTime) {
155
// it has timed out!
156
// call it
157
const callback = currentCallbackConfig.scheduledCallback;
186
- safelyCallScheduledCallback(callback, currentCallbackConfig.callbackId);
158
+ // TODO: error handling
159
+ callback(frameDeadlineObject);
160
+ // remove it from linked list
161
+ cancelScheduledWork(currentCallbackConfig);
162
} else {
163
if (
164
timeoutTime !== -1 &&
168
nextSoonestTimeoutTime = timeoutTime;
169
}
170
}
171
+ currentCallbackConfig = currentCallbackConfig.next;
172
}
173
};
174
184
}
185
isIdleScheduled = false;
186
211
- if (pendingCallbacks.length === 0) {
187
+ if (headOfPendingCallbacksLinkedList === null) {
188
return;
189
}
190
193
194
let currentTime = now();
195
// Next, as long as we have idle time, try calling more callbacks.
220
- while (frameDeadline - currentTime > 0 && pendingCallbacks.length > 0) {
221
- const latestCallbackConfig = pendingCallbacks.shift();
196
+ while (
197
+ frameDeadline - currentTime > 0 &&
198
+ headOfPendingCallbacksLinkedList !== null
199
+ ) {
200
+ const latestCallbackConfig = headOfPendingCallbacksLinkedList;
201
+ // move head of list to next callback
202
+ headOfPendingCallbacksLinkedList = latestCallbackConfig.next;
203
+ if (headOfPendingCallbacksLinkedList !== null) {
204
+ headOfPendingCallbacksLinkedList.prev = null;
205
+ } else {
206
+ // if headOfPendingCallbacksLinkedList is null,
207
+ // then the list must be empty.
208
+ // make sure we set the tail to null as well.
209
+ tailOfPendingCallbacksLinkedList = null;
210
+ }
211
frameDeadlineObject.didTimeout = false;
212
const latestCallback = latestCallbackConfig.scheduledCallback;
224
- const newCallbackId = latestCallbackConfig.callbackId;
225
- safelyCallScheduledCallback(latestCallback, newCallbackId);
213
+ // TODO: before using this outside of React we need to add error handling
214
+ latestCallback(frameDeadlineObject);
215
currentTime = now();
216
}
228
- if (pendingCallbacks.length > 0) {
217
+ if (headOfPendingCallbacksLinkedList !== null) {
218
if (!isAnimationFrameScheduled) {
219
// Schedule another animation callback so we retry later.
220
isAnimationFrameScheduled = true;
260
scheduleWork = function(
261
callback: FrameCallbackType,
262
options?: {timeout: number},
274
- ): number {
263
+ ): CallbackIdType /* CallbackConfigType */ {
264
let timeoutTime = -1;
265
if (options != null && typeof options.timeout === 'number') {
266
timeoutTime = now() + options.timeout;
272
nextSoonestTimeoutTime = timeoutTime;
273
}
274
286
- const newCallbackId = getCallbackId();
287
- const scheduledCallbackConfig = {
275
+ const scheduledCallbackConfig: CallbackConfigType = {
276
scheduledCallback: callback,
289
- callbackId: newCallbackId,
277
timeoutTime,
278
+ prev: null,
279
+ next: null,
280
};
292
- pendingCallbacks.push(scheduledCallbackConfig);
281
+ if (headOfPendingCallbacksLinkedList === null) {
282
+ // Make this callback the head and tail of our list
283
+ headOfPendingCallbacksLinkedList = scheduledCallbackConfig;
284
+ tailOfPendingCallbacksLinkedList = scheduledCallbackConfig;
285
+ } else {
286
+ // Add latest callback as the new tail of the list
287
+ scheduledCallbackConfig.prev = tailOfPendingCallbacksLinkedList;
288
+ // renaming for clarity
289
+ const oldTailOfPendingCallbacksLinkedList = tailOfPendingCallbacksLinkedList;
290
+ if (oldTailOfPendingCallbacksLinkedList !== null) {
291
+ oldTailOfPendingCallbacksLinkedList.next = scheduledCallbackConfig;
292
+ }
293
+ tailOfPendingCallbacksLinkedList = scheduledCallbackConfig;
294
+ }
295
294
- registeredCallbackIds[newCallbackId] = true;
296
if (!isAnimationFrameScheduled) {
297
// If rAF didn't already schedule one, we need to schedule a frame.
298
// TODO: If this rAF doesn't materialize because the browser throttles, we
301
isAnimationFrameScheduled = true;
302
requestAnimationFrameForReact(animationTick);
303
}
303
- return newCallbackId;
304
+ return scheduledCallbackConfig;
305
};
306
306
- cancelScheduledWork = function(callbackId: number) {
307
- delete registeredCallbackIds[callbackId];
307
+ cancelScheduledWork = function(
308
+ callbackConfig: CallbackIdType /* CallbackConfigType */,
309
+ ) {
310
+ /**
311
+ * There are four possible cases:
312
+ * - Head/nodeToRemove/Tail -> null
313
+ * In this case we set Head and Tail to null.
314
+ * - Head -> ... middle nodes... -> Tail/nodeToRemove
315
+ * In this case we point the middle.next to null and put middle as the new
316
+ * Tail.
317
+ * - Head/nodeToRemove -> ...middle nodes... -> Tail
318
+ * In this case we point the middle.prev at null and move the Head to
319
+ * middle.
320
+ * - Head -> ... ?some nodes ... -> nodeToRemove -> ... ?some nodes ... -> Tail
321
+ * In this case we point the Head.next to the Tail and the Tail.prev to
322
+ * the Head.
323
+ */
324
+ const next = callbackConfig.next;
325
+ const prev = callbackConfig.prev;
326
+ if (next !== null) {
327
+ // we have a next
328
+
329
+ if (prev !== null) {
330
+ // we have a prev
331
+
332
+ // callbackConfig is somewhere in the middle of a list of 3 or more nodes.
333
+ prev.next = next;
334
+ next.prev = prev;
335
+ return;
336
+ } else {
337
+ // there is a next but not a previous one;
338
+ // callbackConfig is the head of a list of 2 or more other nodes.
339
+ next.prev = null;
340
+ headOfPendingCallbacksLinkedList = next;
341
+ return;
342
+ }
343
+ } else {
344
+ // there is no next callback config; this must the tail of the list
345
+
346
+ if (prev !== null) {
347
+ // we have a prev
348
+
349
+ // callbackConfig is the tail of a list of 2 or more other nodes.
350
+ prev.next = null;
351
+ tailOfPendingCallbacksLinkedList = prev;
352
+ return;
353
+ } else {
354
+ // there is no previous callback config;
355
+ // callbackConfig is the only thing in the linked list,
356
+ // so both head and tail point to it.
357
+ headOfPendingCallbacksLinkedList = null;
358
+ tailOfPendingCallbacksLinkedList = null;
359
+ return;
360
+ }
361
+ }
362
};
363
}
364