14
* control than requestAnimationFrame and requestIdleCallback.
15
* Current TODO items:
16
* X- Pull out the rIC polyfill built into React
17
- * - Initial test coverage
18
- * - Support for multiple callbacks
17
+ * X- Initial test coverage
18
+ * X- Support for multiple callbacks
19
* - Support for two priorities; serial and deferred
20
* - Better test coverage
21
* - Better docblock
31
// The frame rate is dynamically adjusted.
32
33
import type {Deadline} from 'react-reconciler';
34
+type CallbackConfigType = {|
35
+ scheduledCallback: Deadline => void,
36
+ timeoutTime: number,
37
+ callbackId: number, // used for cancelling
38
+|};
39
40
import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment';
41
import warning from 'fbjs/lib/warning';
90
clearTimeout(timeoutID);
91
};
92
} else {
88
- // Always polyfill requestIdleCallback and cancelIdleCallback
93
+ // We keep callbacks in a queue.
94
+ // Calling rIC 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
90
- let scheduledRICCallback = null;
91
- let isIdleScheduled = false;
92
- let timeoutTime = -1;
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 'cIC' 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} = {};
112
113
+ // We track what the next soonest timeoutTime is, to be able to quickly tell
114
+ // if none of the scheduled callbacks have timed out.
115
+ let nextSoonestTimeoutTime = -1;
116
+
117
+ let isIdleScheduled = false;
118
let isAnimationFrameScheduled = false;
119
120
let frameDeadline = 0;
124
let previousFrameTime = 33;
125
let activeFrameTime = 33;
126
103
- const frameDeadlineObject = {
127
+ const frameDeadlineObject: Deadline = {
128
didTimeout: false,
129
timeRemaining() {
130
const remaining = frameDeadline - now();
132
},
133
};
134
135
+ const safelyCallScheduledCallback = function(callback, callbackId) {
136
+ if (!registeredCallbackIds[callbackId]) {
137
+ // ignore cancelled callbacks
138
+ return;
139
+ }
140
+ try {
141
+ callback(frameDeadlineObject);
142
+ // Avoid using 'catch' to keep errors easy to debug
143
+ } finally {
144
+ // always clean up the callbackId, even if the callback throws
145
+ delete registeredCallbackIds[callbackId];
146
+ }
147
+ };
148
+
149
+ /**
150
+ * Checks for timed out callbacks, runs them, and then checks again to see if
151
+ * any more have timed out.
152
+ * Keeps doing this until there are none which have currently timed out.
153
+ */
154
+ const callTimedOutCallbacks = function() {
155
+ if (pendingCallbacks.length === 0) {
156
+ return;
157
+ }
158
+
159
+ const currentTime = now();
160
+ // TODO: this would be more efficient if deferred callbacks are stored in
161
+ // min heap.
162
+ // Or in a linked list with links for both timeoutTime order and insertion
163
+ // order.
164
+ // For now an easy compromise is the current approach:
165
+ // Keep a pointer to the soonest timeoutTime, and check that first.
166
+ // If it has not expired, we can skip traversing the whole list.
167
+ // If it has expired, then we step through all the callbacks.
168
+ if (nextSoonestTimeoutTime === -1 || nextSoonestTimeoutTime > currentTime) {
169
+ // We know that none of them have timed out yet.
170
+ return;
171
+ }
172
+ nextSoonestTimeoutTime = -1; // we will reset it below
173
+
174
+ // keep checking until we don't find any more timed out callbacks
175
+ frameDeadlineObject.didTimeout = true;
176
+ for (let i = 0, len = pendingCallbacks.length; i < len; i++) {
177
+ const currentCallbackConfig = pendingCallbacks[i];
178
+ const timeoutTime = currentCallbackConfig.timeoutTime;
179
+ if (timeoutTime !== -1 && timeoutTime <= currentTime) {
180
+ // it has timed out!
181
+ // call it
182
+ const callback = currentCallbackConfig.scheduledCallback;
183
+ safelyCallScheduledCallback(callback, timeoutTime);
184
+ } else {
185
+ if (
186
+ timeoutTime !== -1 &&
187
+ (nextSoonestTimeoutTime === -1 ||
188
+ timeoutTime < nextSoonestTimeoutTime)
189
+ ) {
190
+ nextSoonestTimeoutTime = timeoutTime;
191
+ }
192
+ }
193
+ }
194
+ };
195
+
196
// We use the postMessage trick to defer idle work until after the repaint.
197
const messageKey =
198
'__reactIdleCallback$' +
204
return;
205
}
206
207
+ if (pendingCallbacks.length === 0) {
208
+ return;
209
+ }
210
isIdleScheduled = false;
211
124
- const currentTime = now();
125
- if (frameDeadline - currentTime <= 0) {
126
- // There's no time left in this idle period. Check if the callback has
127
- // a timeout and whether it's been exceeded.
128
- if (timeoutTime !== -1 && timeoutTime <= currentTime) {
129
- // Exceeded the timeout. Invoke the callback even though there's no
130
- // time left.
131
- frameDeadlineObject.didTimeout = true;
132
- } else {
133
- // No timeout.
134
- if (!isAnimationFrameScheduled) {
135
- // Schedule another animation callback so we retry later.
136
- isAnimationFrameScheduled = true;
137
- requestAnimationFrame(animationTick);
138
- }
139
- // Exit without invoking the callback.
140
- return;
141
- }
142
- } else {
143
- // There's still time left in this idle period.
212
+ // First call anything which has timed out, until we have caught up.
213
+ callTimedOutCallbacks();
214
+
215
+ let currentTime = now();
216
+ // Next, as long as we have idle time, try calling more callbacks.
217
+ while (frameDeadline - currentTime > 0 && pendingCallbacks.length > 0) {
218
+ const latestCallbackConfig = pendingCallbacks.shift();
219
frameDeadlineObject.didTimeout = false;
220
+ const latestCallback = latestCallbackConfig.scheduledCallback;
221
+ const newCallbackId = latestCallbackConfig.callbackId;
222
+ safelyCallScheduledCallback(latestCallback, newCallbackId);
223
+ currentTime = now();
224
}
146
-
147
- timeoutTime = -1;
148
- const callback = scheduledRICCallback;
149
- scheduledRICCallback = null;
150
- if (callback !== null) {
151
- callback(frameDeadlineObject);
225
+ if (pendingCallbacks.length > 0) {
226
+ if (!isAnimationFrameScheduled) {
227
+ // Schedule another animation callback so we retry later.
228
+ isAnimationFrameScheduled = true;
229
+ requestAnimationFrame(animationTick);
230
+ }
231
}
232
};
233
// Assumes that we have addEventListener in this environment. Might need
269
callback: (deadline: Deadline) => void,
270
options?: {timeout: number},
271
): number {
193
- // This assumes that we only schedule one callback at a time because that's
194
- // how Fiber uses it.
195
- scheduledRICCallback = callback;
272
+ let timeoutTime = -1;
273
if (options != null && typeof options.timeout === 'number') {
274
timeoutTime = now() + options.timeout;
275
}
276
+ if (timeoutTime > nextSoonestTimeoutTime) {
277
+ nextSoonestTimeoutTime = timeoutTime;
278
+ }
279
+
280
+ const newCallbackId = getCallbackId();
281
+ const scheduledCallbackConfig = {
282
+ scheduledCallback: callback,
283
+ callbackId: newCallbackId,
284
+ timeoutTime,
285
+ };
286
+ pendingCallbacks.push(scheduledCallbackConfig);
287
+
288
+ registeredCallbackIds[newCallbackId] = true;
289
if (!isAnimationFrameScheduled) {
290
// If rAF didn't already schedule one, we need to schedule a frame.
291
// TODO: If this rAF doesn't materialize because the browser throttles, we
294
isAnimationFrameScheduled = true;
295
requestAnimationFrame(animationTick);
296
}
207
- return 0;
297
+ return newCallbackId;
298
};
299
210
- cIC = function() {
211
- scheduledRICCallback = null;
212
- isIdleScheduled = false;
213
- timeoutTime = -1;
300
+ cIC = function(callbackId: number) {
301
+ delete registeredCallbackIds[callbackId];
302
};
303
}
304