UNPKG

22.9 kBJavaScriptView Raw
1/** @license React v0.13.1
2 * scheduler.development.js
3 *
4 * Copyright (c) Facebook, Inc. and its affiliates.
5 *
6 * This source code is licensed under the MIT license found in the
7 * LICENSE file in the root directory of this source tree.
8 */
9
10'use strict';
11
12
13
14if (process.env.NODE_ENV !== "production") {
15 (function() {
16'use strict';
17
18Object.defineProperty(exports, '__esModule', { value: true });
19
20var enableSchedulerDebugging = false;
21
22/* eslint-disable no-var */
23
24// TODO: Use symbols?
25var ImmediatePriority = 1;
26var UserBlockingPriority = 2;
27var NormalPriority = 3;
28var LowPriority = 4;
29var IdlePriority = 5;
30
31// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
32// Math.pow(2, 30) - 1
33// 0b111111111111111111111111111111
34var maxSigned31BitInt = 1073741823;
35
36// Times out immediately
37var IMMEDIATE_PRIORITY_TIMEOUT = -1;
38// Eventually times out
39var USER_BLOCKING_PRIORITY = 250;
40var NORMAL_PRIORITY_TIMEOUT = 5000;
41var LOW_PRIORITY_TIMEOUT = 10000;
42// Never times out
43var IDLE_PRIORITY = maxSigned31BitInt;
44
45// Callbacks are stored as a circular, doubly linked list.
46var firstCallbackNode = null;
47
48var currentDidTimeout = false;
49// Pausing the scheduler is useful for debugging.
50var isSchedulerPaused = false;
51
52var currentPriorityLevel = NormalPriority;
53var currentEventStartTime = -1;
54var currentExpirationTime = -1;
55
56// This is set when a callback is being executed, to prevent re-entrancy.
57var isExecutingCallback = false;
58
59var isHostCallbackScheduled = false;
60
61var hasNativePerformanceNow = typeof performance === 'object' && typeof performance.now === 'function';
62
63function ensureHostCallbackIsScheduled() {
64 if (isExecutingCallback) {
65 // Don't schedule work yet; wait until the next time we yield.
66 return;
67 }
68 // Schedule the host callback using the earliest expiration in the list.
69 var expirationTime = firstCallbackNode.expirationTime;
70 if (!isHostCallbackScheduled) {
71 isHostCallbackScheduled = true;
72 } else {
73 // Cancel the existing host callback.
74 cancelHostCallback();
75 }
76 requestHostCallback(flushWork, expirationTime);
77}
78
79function flushFirstCallback() {
80 var flushedNode = firstCallbackNode;
81
82 // Remove the node from the list before calling the callback. That way the
83 // list is in a consistent state even if the callback throws.
84 var next = firstCallbackNode.next;
85 if (firstCallbackNode === next) {
86 // This is the last callback in the list.
87 firstCallbackNode = null;
88 next = null;
89 } else {
90 var lastCallbackNode = firstCallbackNode.previous;
91 firstCallbackNode = lastCallbackNode.next = next;
92 next.previous = lastCallbackNode;
93 }
94
95 flushedNode.next = flushedNode.previous = null;
96
97 // Now it's safe to call the callback.
98 var callback = flushedNode.callback;
99 var expirationTime = flushedNode.expirationTime;
100 var priorityLevel = flushedNode.priorityLevel;
101 var previousPriorityLevel = currentPriorityLevel;
102 var previousExpirationTime = currentExpirationTime;
103 currentPriorityLevel = priorityLevel;
104 currentExpirationTime = expirationTime;
105 var continuationCallback;
106 try {
107 continuationCallback = callback();
108 } finally {
109 currentPriorityLevel = previousPriorityLevel;
110 currentExpirationTime = previousExpirationTime;
111 }
112
113 // A callback may return a continuation. The continuation should be scheduled
114 // with the same priority and expiration as the just-finished callback.
115 if (typeof continuationCallback === 'function') {
116 var continuationNode = {
117 callback: continuationCallback,
118 priorityLevel: priorityLevel,
119 expirationTime: expirationTime,
120 next: null,
121 previous: null
122 };
123
124 // Insert the new callback into the list, sorted by its expiration. This is
125 // almost the same as the code in `scheduleCallback`, except the callback
126 // is inserted into the list *before* callbacks of equal expiration instead
127 // of after.
128 if (firstCallbackNode === null) {
129 // This is the first callback in the list.
130 firstCallbackNode = continuationNode.next = continuationNode.previous = continuationNode;
131 } else {
132 var nextAfterContinuation = null;
133 var node = firstCallbackNode;
134 do {
135 if (node.expirationTime >= expirationTime) {
136 // This callback expires at or after the continuation. We will insert
137 // the continuation *before* this callback.
138 nextAfterContinuation = node;
139 break;
140 }
141 node = node.next;
142 } while (node !== firstCallbackNode);
143
144 if (nextAfterContinuation === null) {
145 // No equal or lower priority callback was found, which means the new
146 // callback is the lowest priority callback in the list.
147 nextAfterContinuation = firstCallbackNode;
148 } else if (nextAfterContinuation === firstCallbackNode) {
149 // The new callback is the highest priority callback in the list.
150 firstCallbackNode = continuationNode;
151 ensureHostCallbackIsScheduled();
152 }
153
154 var previous = nextAfterContinuation.previous;
155 previous.next = nextAfterContinuation.previous = continuationNode;
156 continuationNode.next = nextAfterContinuation;
157 continuationNode.previous = previous;
158 }
159 }
160}
161
162function flushImmediateWork() {
163 if (
164 // Confirm we've exited the outer most event handler
165 currentEventStartTime === -1 && firstCallbackNode !== null && firstCallbackNode.priorityLevel === ImmediatePriority) {
166 isExecutingCallback = true;
167 try {
168 do {
169 flushFirstCallback();
170 } while (
171 // Keep flushing until there are no more immediate callbacks
172 firstCallbackNode !== null && firstCallbackNode.priorityLevel === ImmediatePriority);
173 } finally {
174 isExecutingCallback = false;
175 if (firstCallbackNode !== null) {
176 // There's still work remaining. Request another callback.
177 ensureHostCallbackIsScheduled();
178 } else {
179 isHostCallbackScheduled = false;
180 }
181 }
182 }
183}
184
185function flushWork(didTimeout) {
186 // Exit right away if we're currently paused
187
188 if (enableSchedulerDebugging && isSchedulerPaused) {
189 return;
190 }
191
192 isExecutingCallback = true;
193 var previousDidTimeout = currentDidTimeout;
194 currentDidTimeout = didTimeout;
195 try {
196 if (didTimeout) {
197 // Flush all the expired callbacks without yielding.
198 while (firstCallbackNode !== null && !(enableSchedulerDebugging && isSchedulerPaused)) {
199 // TODO Wrap in feature flag
200 // Read the current time. Flush all the callbacks that expire at or
201 // earlier than that time. Then read the current time again and repeat.
202 // This optimizes for as few performance.now calls as possible.
203 var currentTime = exports.unstable_now();
204 if (firstCallbackNode.expirationTime <= currentTime) {
205 do {
206 flushFirstCallback();
207 } while (firstCallbackNode !== null && firstCallbackNode.expirationTime <= currentTime && !(enableSchedulerDebugging && isSchedulerPaused));
208 continue;
209 }
210 break;
211 }
212 } else {
213 // Keep flushing callbacks until we run out of time in the frame.
214 if (firstCallbackNode !== null) {
215 do {
216 if (enableSchedulerDebugging && isSchedulerPaused) {
217 break;
218 }
219 flushFirstCallback();
220 } while (firstCallbackNode !== null && !shouldYieldToHost());
221 }
222 }
223 } finally {
224 isExecutingCallback = false;
225 currentDidTimeout = previousDidTimeout;
226 if (firstCallbackNode !== null) {
227 // There's still work remaining. Request another callback.
228 ensureHostCallbackIsScheduled();
229 } else {
230 isHostCallbackScheduled = false;
231 }
232 // Before exiting, flush all the immediate work that was scheduled.
233 flushImmediateWork();
234 }
235}
236
237function unstable_runWithPriority(priorityLevel, eventHandler) {
238 switch (priorityLevel) {
239 case ImmediatePriority:
240 case UserBlockingPriority:
241 case NormalPriority:
242 case LowPriority:
243 case IdlePriority:
244 break;
245 default:
246 priorityLevel = NormalPriority;
247 }
248
249 var previousPriorityLevel = currentPriorityLevel;
250 var previousEventStartTime = currentEventStartTime;
251 currentPriorityLevel = priorityLevel;
252 currentEventStartTime = exports.unstable_now();
253
254 try {
255 return eventHandler();
256 } finally {
257 currentPriorityLevel = previousPriorityLevel;
258 currentEventStartTime = previousEventStartTime;
259
260 // Before exiting, flush all the immediate work that was scheduled.
261 flushImmediateWork();
262 }
263}
264
265function unstable_wrapCallback(callback) {
266 var parentPriorityLevel = currentPriorityLevel;
267 return function () {
268 // This is a fork of runWithPriority, inlined for performance.
269 var previousPriorityLevel = currentPriorityLevel;
270 var previousEventStartTime = currentEventStartTime;
271 currentPriorityLevel = parentPriorityLevel;
272 currentEventStartTime = exports.unstable_now();
273
274 try {
275 return callback.apply(this, arguments);
276 } finally {
277 currentPriorityLevel = previousPriorityLevel;
278 currentEventStartTime = previousEventStartTime;
279 flushImmediateWork();
280 }
281 };
282}
283
284function unstable_scheduleCallback(callback, deprecated_options) {
285 var startTime = currentEventStartTime !== -1 ? currentEventStartTime : exports.unstable_now();
286
287 var expirationTime;
288 if (typeof deprecated_options === 'object' && deprecated_options !== null && typeof deprecated_options.timeout === 'number') {
289 // FIXME: Remove this branch once we lift expiration times out of React.
290 expirationTime = startTime + deprecated_options.timeout;
291 } else {
292 switch (currentPriorityLevel) {
293 case ImmediatePriority:
294 expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
295 break;
296 case UserBlockingPriority:
297 expirationTime = startTime + USER_BLOCKING_PRIORITY;
298 break;
299 case IdlePriority:
300 expirationTime = startTime + IDLE_PRIORITY;
301 break;
302 case LowPriority:
303 expirationTime = startTime + LOW_PRIORITY_TIMEOUT;
304 break;
305 case NormalPriority:
306 default:
307 expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;
308 }
309 }
310
311 var newNode = {
312 callback: callback,
313 priorityLevel: currentPriorityLevel,
314 expirationTime: expirationTime,
315 next: null,
316 previous: null
317 };
318
319 // Insert the new callback into the list, ordered first by expiration, then
320 // by insertion. So the new callback is inserted any other callback with
321 // equal expiration.
322 if (firstCallbackNode === null) {
323 // This is the first callback in the list.
324 firstCallbackNode = newNode.next = newNode.previous = newNode;
325 ensureHostCallbackIsScheduled();
326 } else {
327 var next = null;
328 var node = firstCallbackNode;
329 do {
330 if (node.expirationTime > expirationTime) {
331 // The new callback expires before this one.
332 next = node;
333 break;
334 }
335 node = node.next;
336 } while (node !== firstCallbackNode);
337
338 if (next === null) {
339 // No callback with a later expiration was found, which means the new
340 // callback has the latest expiration in the list.
341 next = firstCallbackNode;
342 } else if (next === firstCallbackNode) {
343 // The new callback has the earliest expiration in the entire list.
344 firstCallbackNode = newNode;
345 ensureHostCallbackIsScheduled();
346 }
347
348 var previous = next.previous;
349 previous.next = next.previous = newNode;
350 newNode.next = next;
351 newNode.previous = previous;
352 }
353
354 return newNode;
355}
356
357function unstable_pauseExecution() {
358 isSchedulerPaused = true;
359}
360
361function unstable_continueExecution() {
362 isSchedulerPaused = false;
363 if (firstCallbackNode !== null) {
364 ensureHostCallbackIsScheduled();
365 }
366}
367
368function unstable_getFirstCallbackNode() {
369 return firstCallbackNode;
370}
371
372function unstable_cancelCallback(callbackNode) {
373 var next = callbackNode.next;
374 if (next === null) {
375 // Already cancelled.
376 return;
377 }
378
379 if (next === callbackNode) {
380 // This is the only scheduled callback. Clear the list.
381 firstCallbackNode = null;
382 } else {
383 // Remove the callback from its position in the list.
384 if (callbackNode === firstCallbackNode) {
385 firstCallbackNode = next;
386 }
387 var previous = callbackNode.previous;
388 previous.next = next;
389 next.previous = previous;
390 }
391
392 callbackNode.next = callbackNode.previous = null;
393}
394
395function unstable_getCurrentPriorityLevel() {
396 return currentPriorityLevel;
397}
398
399function unstable_shouldYield() {
400 return !currentDidTimeout && (firstCallbackNode !== null && firstCallbackNode.expirationTime < currentExpirationTime || shouldYieldToHost());
401}
402
403// The remaining code is essentially a polyfill for requestIdleCallback. It
404// works by scheduling a requestAnimationFrame, storing the time for the start
405// of the frame, then scheduling a postMessage which gets scheduled after paint.
406// Within the postMessage handler do as much work as possible until time + frame
407// rate. By separating the idle call into a separate event tick we ensure that
408// layout, paint and other browser work is counted against the available time.
409// The frame rate is dynamically adjusted.
410
411// We capture a local reference to any global, in case it gets polyfilled after
412// this module is initially evaluated. We want to be using a
413// consistent implementation.
414var localDate = Date;
415
416// This initialization code may run even on server environments if a component
417// just imports ReactDOM (e.g. for findDOMNode). Some environments might not
418// have setTimeout or clearTimeout. However, we always expect them to be defined
419// on the client. https://github.com/facebook/react/pull/13088
420var localSetTimeout = typeof setTimeout === 'function' ? setTimeout : undefined;
421var localClearTimeout = typeof clearTimeout === 'function' ? clearTimeout : undefined;
422
423// We don't expect either of these to necessarily be defined, but we will error
424// later if they are missing on the client.
425var localRequestAnimationFrame = typeof requestAnimationFrame === 'function' ? requestAnimationFrame : undefined;
426var localCancelAnimationFrame = typeof cancelAnimationFrame === 'function' ? cancelAnimationFrame : undefined;
427
428// requestAnimationFrame does not run when the tab is in the background. If
429// we're backgrounded we prefer for that work to happen so that the page
430// continues to load in the background. So we also schedule a 'setTimeout' as
431// a fallback.
432// TODO: Need a better heuristic for backgrounded work.
433var ANIMATION_FRAME_TIMEOUT = 100;
434var rAFID;
435var rAFTimeoutID;
436var requestAnimationFrameWithTimeout = function (callback) {
437 // schedule rAF and also a setTimeout
438 rAFID = localRequestAnimationFrame(function (timestamp) {
439 // cancel the setTimeout
440 localClearTimeout(rAFTimeoutID);
441 callback(timestamp);
442 });
443 rAFTimeoutID = localSetTimeout(function () {
444 // cancel the requestAnimationFrame
445 localCancelAnimationFrame(rAFID);
446 callback(exports.unstable_now());
447 }, ANIMATION_FRAME_TIMEOUT);
448};
449
450if (hasNativePerformanceNow) {
451 var Performance = performance;
452 exports.unstable_now = function () {
453 return Performance.now();
454 };
455} else {
456 exports.unstable_now = function () {
457 return localDate.now();
458 };
459}
460
461var requestHostCallback;
462var cancelHostCallback;
463var shouldYieldToHost;
464
465var globalValue = null;
466if (typeof window !== 'undefined') {
467 globalValue = window;
468} else if (typeof global !== 'undefined') {
469 globalValue = global;
470}
471
472if (globalValue && globalValue._schedMock) {
473 // Dynamic injection, only for testing purposes.
474 var globalImpl = globalValue._schedMock;
475 requestHostCallback = globalImpl[0];
476 cancelHostCallback = globalImpl[1];
477 shouldYieldToHost = globalImpl[2];
478 exports.unstable_now = globalImpl[3];
479} else if (
480// If Scheduler runs in a non-DOM environment, it falls back to a naive
481// implementation using setTimeout.
482typeof window === 'undefined' ||
483// Check if MessageChannel is supported, too.
484typeof MessageChannel !== 'function') {
485 // If this accidentally gets imported in a non-browser environment, e.g. JavaScriptCore,
486 // fallback to a naive implementation.
487 var _callback = null;
488 var _flushCallback = function (didTimeout) {
489 if (_callback !== null) {
490 try {
491 _callback(didTimeout);
492 } finally {
493 _callback = null;
494 }
495 }
496 };
497 requestHostCallback = function (cb, ms) {
498 if (_callback !== null) {
499 // Protect against re-entrancy.
500 setTimeout(requestHostCallback, 0, cb);
501 } else {
502 _callback = cb;
503 setTimeout(_flushCallback, 0, false);
504 }
505 };
506 cancelHostCallback = function () {
507 _callback = null;
508 };
509 shouldYieldToHost = function () {
510 return false;
511 };
512} else {
513 if (typeof console !== 'undefined') {
514 // TODO: Remove fb.me link
515 if (typeof localRequestAnimationFrame !== 'function') {
516 console.error("This browser doesn't support requestAnimationFrame. " + 'Make sure that you load a ' + 'polyfill in older browsers. https://fb.me/react-polyfills');
517 }
518 if (typeof localCancelAnimationFrame !== 'function') {
519 console.error("This browser doesn't support cancelAnimationFrame. " + 'Make sure that you load a ' + 'polyfill in older browsers. https://fb.me/react-polyfills');
520 }
521 }
522
523 var scheduledHostCallback = null;
524 var isMessageEventScheduled = false;
525 var timeoutTime = -1;
526
527 var isAnimationFrameScheduled = false;
528
529 var isFlushingHostCallback = false;
530
531 var frameDeadline = 0;
532 // We start out assuming that we run at 30fps but then the heuristic tracking
533 // will adjust this value to a faster fps if we get more frequent animation
534 // frames.
535 var previousFrameTime = 33;
536 var activeFrameTime = 33;
537
538 shouldYieldToHost = function () {
539 return frameDeadline <= exports.unstable_now();
540 };
541
542 // We use the postMessage trick to defer idle work until after the repaint.
543 var channel = new MessageChannel();
544 var port = channel.port2;
545 channel.port1.onmessage = function (event) {
546 isMessageEventScheduled = false;
547
548 var prevScheduledCallback = scheduledHostCallback;
549 var prevTimeoutTime = timeoutTime;
550 scheduledHostCallback = null;
551 timeoutTime = -1;
552
553 var currentTime = exports.unstable_now();
554
555 var didTimeout = false;
556 if (frameDeadline - currentTime <= 0) {
557 // There's no time left in this idle period. Check if the callback has
558 // a timeout and whether it's been exceeded.
559 if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
560 // Exceeded the timeout. Invoke the callback even though there's no
561 // time left.
562 didTimeout = true;
563 } else {
564 // No timeout.
565 if (!isAnimationFrameScheduled) {
566 // Schedule another animation callback so we retry later.
567 isAnimationFrameScheduled = true;
568 requestAnimationFrameWithTimeout(animationTick);
569 }
570 // Exit without invoking the callback.
571 scheduledHostCallback = prevScheduledCallback;
572 timeoutTime = prevTimeoutTime;
573 return;
574 }
575 }
576
577 if (prevScheduledCallback !== null) {
578 isFlushingHostCallback = true;
579 try {
580 prevScheduledCallback(didTimeout);
581 } finally {
582 isFlushingHostCallback = false;
583 }
584 }
585 };
586
587 var animationTick = function (rafTime) {
588 if (scheduledHostCallback !== null) {
589 // Eagerly schedule the next animation callback at the beginning of the
590 // frame. If the scheduler queue is not empty at the end of the frame, it
591 // will continue flushing inside that callback. If the queue *is* empty,
592 // then it will exit immediately. Posting the callback at the start of the
593 // frame ensures it's fired within the earliest possible frame. If we
594 // waited until the end of the frame to post the callback, we risk the
595 // browser skipping a frame and not firing the callback until the frame
596 // after that.
597 requestAnimationFrameWithTimeout(animationTick);
598 } else {
599 // No pending work. Exit.
600 isAnimationFrameScheduled = false;
601 return;
602 }
603
604 var nextFrameTime = rafTime - frameDeadline + activeFrameTime;
605 if (nextFrameTime < activeFrameTime && previousFrameTime < activeFrameTime) {
606 if (nextFrameTime < 8) {
607 // Defensive coding. We don't support higher frame rates than 120hz.
608 // If the calculated frame time gets lower than 8, it is probably a bug.
609 nextFrameTime = 8;
610 }
611 // If one frame goes long, then the next one can be short to catch up.
612 // If two frames are short in a row, then that's an indication that we
613 // actually have a higher frame rate than what we're currently optimizing.
614 // We adjust our heuristic dynamically accordingly. For example, if we're
615 // running on 120hz display or 90hz VR display.
616 // Take the max of the two in case one of them was an anomaly due to
617 // missed frame deadlines.
618 activeFrameTime = nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
619 } else {
620 previousFrameTime = nextFrameTime;
621 }
622 frameDeadline = rafTime + activeFrameTime;
623 if (!isMessageEventScheduled) {
624 isMessageEventScheduled = true;
625 port.postMessage(undefined);
626 }
627 };
628
629 requestHostCallback = function (callback, absoluteTimeout) {
630 scheduledHostCallback = callback;
631 timeoutTime = absoluteTimeout;
632 if (isFlushingHostCallback || absoluteTimeout < 0) {
633 // Don't wait for the next frame. Continue working ASAP, in a new event.
634 port.postMessage(undefined);
635 } else if (!isAnimationFrameScheduled) {
636 // If rAF didn't already schedule one, we need to schedule a frame.
637 // TODO: If this rAF doesn't materialize because the browser throttles, we
638 // might want to still have setTimeout trigger rIC as a backup to ensure
639 // that we keep performing work.
640 isAnimationFrameScheduled = true;
641 requestAnimationFrameWithTimeout(animationTick);
642 }
643 };
644
645 cancelHostCallback = function () {
646 scheduledHostCallback = null;
647 isMessageEventScheduled = false;
648 timeoutTime = -1;
649 };
650}
651
652exports.unstable_ImmediatePriority = ImmediatePriority;
653exports.unstable_UserBlockingPriority = UserBlockingPriority;
654exports.unstable_NormalPriority = NormalPriority;
655exports.unstable_IdlePriority = IdlePriority;
656exports.unstable_LowPriority = LowPriority;
657exports.unstable_runWithPriority = unstable_runWithPriority;
658exports.unstable_scheduleCallback = unstable_scheduleCallback;
659exports.unstable_cancelCallback = unstable_cancelCallback;
660exports.unstable_wrapCallback = unstable_wrapCallback;
661exports.unstable_getCurrentPriorityLevel = unstable_getCurrentPriorityLevel;
662exports.unstable_shouldYield = unstable_shouldYield;
663exports.unstable_continueExecution = unstable_continueExecution;
664exports.unstable_pauseExecution = unstable_pauseExecution;
665exports.unstable_getFirstCallbackNode = unstable_getFirstCallbackNode;
666 })();
667}