UNPKG

23.9 kBJavaScriptView Raw
1/** @license React v0.13.2
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_next(eventHandler) {
266 var priorityLevel = void 0;
267 switch (currentPriorityLevel) {
268 case ImmediatePriority:
269 case UserBlockingPriority:
270 case NormalPriority:
271 // Shift down to normal priority
272 priorityLevel = NormalPriority;
273 break;
274 default:
275 // Anything lower than normal priority should remain at the current level.
276 priorityLevel = currentPriorityLevel;
277 break;
278 }
279
280 var previousPriorityLevel = currentPriorityLevel;
281 var previousEventStartTime = currentEventStartTime;
282 currentPriorityLevel = priorityLevel;
283 currentEventStartTime = exports.unstable_now();
284
285 try {
286 return eventHandler();
287 } finally {
288 currentPriorityLevel = previousPriorityLevel;
289 currentEventStartTime = previousEventStartTime;
290
291 // Before exiting, flush all the immediate work that was scheduled.
292 flushImmediateWork();
293 }
294}
295
296function unstable_wrapCallback(callback) {
297 var parentPriorityLevel = currentPriorityLevel;
298 return function () {
299 // This is a fork of runWithPriority, inlined for performance.
300 var previousPriorityLevel = currentPriorityLevel;
301 var previousEventStartTime = currentEventStartTime;
302 currentPriorityLevel = parentPriorityLevel;
303 currentEventStartTime = exports.unstable_now();
304
305 try {
306 return callback.apply(this, arguments);
307 } finally {
308 currentPriorityLevel = previousPriorityLevel;
309 currentEventStartTime = previousEventStartTime;
310 flushImmediateWork();
311 }
312 };
313}
314
315function unstable_scheduleCallback(callback, deprecated_options) {
316 var startTime = currentEventStartTime !== -1 ? currentEventStartTime : exports.unstable_now();
317
318 var expirationTime;
319 if (typeof deprecated_options === 'object' && deprecated_options !== null && typeof deprecated_options.timeout === 'number') {
320 // FIXME: Remove this branch once we lift expiration times out of React.
321 expirationTime = startTime + deprecated_options.timeout;
322 } else {
323 switch (currentPriorityLevel) {
324 case ImmediatePriority:
325 expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
326 break;
327 case UserBlockingPriority:
328 expirationTime = startTime + USER_BLOCKING_PRIORITY;
329 break;
330 case IdlePriority:
331 expirationTime = startTime + IDLE_PRIORITY;
332 break;
333 case LowPriority:
334 expirationTime = startTime + LOW_PRIORITY_TIMEOUT;
335 break;
336 case NormalPriority:
337 default:
338 expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;
339 }
340 }
341
342 var newNode = {
343 callback: callback,
344 priorityLevel: currentPriorityLevel,
345 expirationTime: expirationTime,
346 next: null,
347 previous: null
348 };
349
350 // Insert the new callback into the list, ordered first by expiration, then
351 // by insertion. So the new callback is inserted any other callback with
352 // equal expiration.
353 if (firstCallbackNode === null) {
354 // This is the first callback in the list.
355 firstCallbackNode = newNode.next = newNode.previous = newNode;
356 ensureHostCallbackIsScheduled();
357 } else {
358 var next = null;
359 var node = firstCallbackNode;
360 do {
361 if (node.expirationTime > expirationTime) {
362 // The new callback expires before this one.
363 next = node;
364 break;
365 }
366 node = node.next;
367 } while (node !== firstCallbackNode);
368
369 if (next === null) {
370 // No callback with a later expiration was found, which means the new
371 // callback has the latest expiration in the list.
372 next = firstCallbackNode;
373 } else if (next === firstCallbackNode) {
374 // The new callback has the earliest expiration in the entire list.
375 firstCallbackNode = newNode;
376 ensureHostCallbackIsScheduled();
377 }
378
379 var previous = next.previous;
380 previous.next = next.previous = newNode;
381 newNode.next = next;
382 newNode.previous = previous;
383 }
384
385 return newNode;
386}
387
388function unstable_pauseExecution() {
389 isSchedulerPaused = true;
390}
391
392function unstable_continueExecution() {
393 isSchedulerPaused = false;
394 if (firstCallbackNode !== null) {
395 ensureHostCallbackIsScheduled();
396 }
397}
398
399function unstable_getFirstCallbackNode() {
400 return firstCallbackNode;
401}
402
403function unstable_cancelCallback(callbackNode) {
404 var next = callbackNode.next;
405 if (next === null) {
406 // Already cancelled.
407 return;
408 }
409
410 if (next === callbackNode) {
411 // This is the only scheduled callback. Clear the list.
412 firstCallbackNode = null;
413 } else {
414 // Remove the callback from its position in the list.
415 if (callbackNode === firstCallbackNode) {
416 firstCallbackNode = next;
417 }
418 var previous = callbackNode.previous;
419 previous.next = next;
420 next.previous = previous;
421 }
422
423 callbackNode.next = callbackNode.previous = null;
424}
425
426function unstable_getCurrentPriorityLevel() {
427 return currentPriorityLevel;
428}
429
430function unstable_shouldYield() {
431 return !currentDidTimeout && (firstCallbackNode !== null && firstCallbackNode.expirationTime < currentExpirationTime || shouldYieldToHost());
432}
433
434// The remaining code is essentially a polyfill for requestIdleCallback. It
435// works by scheduling a requestAnimationFrame, storing the time for the start
436// of the frame, then scheduling a postMessage which gets scheduled after paint.
437// Within the postMessage handler do as much work as possible until time + frame
438// rate. By separating the idle call into a separate event tick we ensure that
439// layout, paint and other browser work is counted against the available time.
440// The frame rate is dynamically adjusted.
441
442// We capture a local reference to any global, in case it gets polyfilled after
443// this module is initially evaluated. We want to be using a
444// consistent implementation.
445var localDate = Date;
446
447// This initialization code may run even on server environments if a component
448// just imports ReactDOM (e.g. for findDOMNode). Some environments might not
449// have setTimeout or clearTimeout. However, we always expect them to be defined
450// on the client. https://github.com/facebook/react/pull/13088
451var localSetTimeout = typeof setTimeout === 'function' ? setTimeout : undefined;
452var localClearTimeout = typeof clearTimeout === 'function' ? clearTimeout : undefined;
453
454// We don't expect either of these to necessarily be defined, but we will error
455// later if they are missing on the client.
456var localRequestAnimationFrame = typeof requestAnimationFrame === 'function' ? requestAnimationFrame : undefined;
457var localCancelAnimationFrame = typeof cancelAnimationFrame === 'function' ? cancelAnimationFrame : undefined;
458
459// requestAnimationFrame does not run when the tab is in the background. If
460// we're backgrounded we prefer for that work to happen so that the page
461// continues to load in the background. So we also schedule a 'setTimeout' as
462// a fallback.
463// TODO: Need a better heuristic for backgrounded work.
464var ANIMATION_FRAME_TIMEOUT = 100;
465var rAFID;
466var rAFTimeoutID;
467var requestAnimationFrameWithTimeout = function (callback) {
468 // schedule rAF and also a setTimeout
469 rAFID = localRequestAnimationFrame(function (timestamp) {
470 // cancel the setTimeout
471 localClearTimeout(rAFTimeoutID);
472 callback(timestamp);
473 });
474 rAFTimeoutID = localSetTimeout(function () {
475 // cancel the requestAnimationFrame
476 localCancelAnimationFrame(rAFID);
477 callback(exports.unstable_now());
478 }, ANIMATION_FRAME_TIMEOUT);
479};
480
481if (hasNativePerformanceNow) {
482 var Performance = performance;
483 exports.unstable_now = function () {
484 return Performance.now();
485 };
486} else {
487 exports.unstable_now = function () {
488 return localDate.now();
489 };
490}
491
492var requestHostCallback;
493var cancelHostCallback;
494var shouldYieldToHost;
495
496var globalValue = null;
497if (typeof window !== 'undefined') {
498 globalValue = window;
499} else if (typeof global !== 'undefined') {
500 globalValue = global;
501}
502
503if (globalValue && globalValue._schedMock) {
504 // Dynamic injection, only for testing purposes.
505 var globalImpl = globalValue._schedMock;
506 requestHostCallback = globalImpl[0];
507 cancelHostCallback = globalImpl[1];
508 shouldYieldToHost = globalImpl[2];
509 exports.unstable_now = globalImpl[3];
510} else if (
511// If Scheduler runs in a non-DOM environment, it falls back to a naive
512// implementation using setTimeout.
513typeof window === 'undefined' ||
514// Check if MessageChannel is supported, too.
515typeof MessageChannel !== 'function') {
516 // If this accidentally gets imported in a non-browser environment, e.g. JavaScriptCore,
517 // fallback to a naive implementation.
518 var _callback = null;
519 var _flushCallback = function (didTimeout) {
520 if (_callback !== null) {
521 try {
522 _callback(didTimeout);
523 } finally {
524 _callback = null;
525 }
526 }
527 };
528 requestHostCallback = function (cb, ms) {
529 if (_callback !== null) {
530 // Protect against re-entrancy.
531 setTimeout(requestHostCallback, 0, cb);
532 } else {
533 _callback = cb;
534 setTimeout(_flushCallback, 0, false);
535 }
536 };
537 cancelHostCallback = function () {
538 _callback = null;
539 };
540 shouldYieldToHost = function () {
541 return false;
542 };
543} else {
544 if (typeof console !== 'undefined') {
545 // TODO: Remove fb.me link
546 if (typeof localRequestAnimationFrame !== 'function') {
547 console.error("This browser doesn't support requestAnimationFrame. " + 'Make sure that you load a ' + 'polyfill in older browsers. https://fb.me/react-polyfills');
548 }
549 if (typeof localCancelAnimationFrame !== 'function') {
550 console.error("This browser doesn't support cancelAnimationFrame. " + 'Make sure that you load a ' + 'polyfill in older browsers. https://fb.me/react-polyfills');
551 }
552 }
553
554 var scheduledHostCallback = null;
555 var isMessageEventScheduled = false;
556 var timeoutTime = -1;
557
558 var isAnimationFrameScheduled = false;
559
560 var isFlushingHostCallback = false;
561
562 var frameDeadline = 0;
563 // We start out assuming that we run at 30fps but then the heuristic tracking
564 // will adjust this value to a faster fps if we get more frequent animation
565 // frames.
566 var previousFrameTime = 33;
567 var activeFrameTime = 33;
568
569 shouldYieldToHost = function () {
570 return frameDeadline <= exports.unstable_now();
571 };
572
573 // We use the postMessage trick to defer idle work until after the repaint.
574 var channel = new MessageChannel();
575 var port = channel.port2;
576 channel.port1.onmessage = function (event) {
577 isMessageEventScheduled = false;
578
579 var prevScheduledCallback = scheduledHostCallback;
580 var prevTimeoutTime = timeoutTime;
581 scheduledHostCallback = null;
582 timeoutTime = -1;
583
584 var currentTime = exports.unstable_now();
585
586 var didTimeout = false;
587 if (frameDeadline - currentTime <= 0) {
588 // There's no time left in this idle period. Check if the callback has
589 // a timeout and whether it's been exceeded.
590 if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
591 // Exceeded the timeout. Invoke the callback even though there's no
592 // time left.
593 didTimeout = true;
594 } else {
595 // No timeout.
596 if (!isAnimationFrameScheduled) {
597 // Schedule another animation callback so we retry later.
598 isAnimationFrameScheduled = true;
599 requestAnimationFrameWithTimeout(animationTick);
600 }
601 // Exit without invoking the callback.
602 scheduledHostCallback = prevScheduledCallback;
603 timeoutTime = prevTimeoutTime;
604 return;
605 }
606 }
607
608 if (prevScheduledCallback !== null) {
609 isFlushingHostCallback = true;
610 try {
611 prevScheduledCallback(didTimeout);
612 } finally {
613 isFlushingHostCallback = false;
614 }
615 }
616 };
617
618 var animationTick = function (rafTime) {
619 if (scheduledHostCallback !== null) {
620 // Eagerly schedule the next animation callback at the beginning of the
621 // frame. If the scheduler queue is not empty at the end of the frame, it
622 // will continue flushing inside that callback. If the queue *is* empty,
623 // then it will exit immediately. Posting the callback at the start of the
624 // frame ensures it's fired within the earliest possible frame. If we
625 // waited until the end of the frame to post the callback, we risk the
626 // browser skipping a frame and not firing the callback until the frame
627 // after that.
628 requestAnimationFrameWithTimeout(animationTick);
629 } else {
630 // No pending work. Exit.
631 isAnimationFrameScheduled = false;
632 return;
633 }
634
635 var nextFrameTime = rafTime - frameDeadline + activeFrameTime;
636 if (nextFrameTime < activeFrameTime && previousFrameTime < activeFrameTime) {
637 if (nextFrameTime < 8) {
638 // Defensive coding. We don't support higher frame rates than 120hz.
639 // If the calculated frame time gets lower than 8, it is probably a bug.
640 nextFrameTime = 8;
641 }
642 // If one frame goes long, then the next one can be short to catch up.
643 // If two frames are short in a row, then that's an indication that we
644 // actually have a higher frame rate than what we're currently optimizing.
645 // We adjust our heuristic dynamically accordingly. For example, if we're
646 // running on 120hz display or 90hz VR display.
647 // Take the max of the two in case one of them was an anomaly due to
648 // missed frame deadlines.
649 activeFrameTime = nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
650 } else {
651 previousFrameTime = nextFrameTime;
652 }
653 frameDeadline = rafTime + activeFrameTime;
654 if (!isMessageEventScheduled) {
655 isMessageEventScheduled = true;
656 port.postMessage(undefined);
657 }
658 };
659
660 requestHostCallback = function (callback, absoluteTimeout) {
661 scheduledHostCallback = callback;
662 timeoutTime = absoluteTimeout;
663 if (isFlushingHostCallback || absoluteTimeout < 0) {
664 // Don't wait for the next frame. Continue working ASAP, in a new event.
665 port.postMessage(undefined);
666 } else if (!isAnimationFrameScheduled) {
667 // If rAF didn't already schedule one, we need to schedule a frame.
668 // TODO: If this rAF doesn't materialize because the browser throttles, we
669 // might want to still have setTimeout trigger rIC as a backup to ensure
670 // that we keep performing work.
671 isAnimationFrameScheduled = true;
672 requestAnimationFrameWithTimeout(animationTick);
673 }
674 };
675
676 cancelHostCallback = function () {
677 scheduledHostCallback = null;
678 isMessageEventScheduled = false;
679 timeoutTime = -1;
680 };
681}
682
683exports.unstable_ImmediatePriority = ImmediatePriority;
684exports.unstable_UserBlockingPriority = UserBlockingPriority;
685exports.unstable_NormalPriority = NormalPriority;
686exports.unstable_IdlePriority = IdlePriority;
687exports.unstable_LowPriority = LowPriority;
688exports.unstable_runWithPriority = unstable_runWithPriority;
689exports.unstable_next = unstable_next;
690exports.unstable_scheduleCallback = unstable_scheduleCallback;
691exports.unstable_cancelCallback = unstable_cancelCallback;
692exports.unstable_wrapCallback = unstable_wrapCallback;
693exports.unstable_getCurrentPriorityLevel = unstable_getCurrentPriorityLevel;
694exports.unstable_shouldYield = unstable_shouldYield;
695exports.unstable_continueExecution = unstable_continueExecution;
696exports.unstable_pauseExecution = unstable_pauseExecution;
697exports.unstable_getFirstCallbackNode = unstable_getFirstCallbackNode;
698 })();
699}