diff --git a/fixtures/fiber-triangle/index.html b/fixtures/fiber-triangle/index.html
index d2541eee1e93..1b5941f09b31 100644
--- a/fixtures/fiber-triangle/index.html
+++ b/fixtures/fiber-triangle/index.html
@@ -76,51 +76,54 @@
Fiber Example
}
}
- function SierpinskiTriangle({ x, y, s, children }) {
- if (s <= targetSize) {
- return (
-
+ class SierpinskiTriangle extends React.Component {
+ shouldComponentUpdate(nextProps) {
+ var o = this.props;
+ var n = nextProps;
+ return !(
+ o.x === n.x &&
+ o.y === n.y &&
+ o.s === n.s &&
+ o.children === n.children
);
- return r;
}
- var newSize = s / 2;
- var slowDown = true;
- if (slowDown) {
- var e = performance.now() + 0.8;
- while (performance.now() < e) {
- // Artificially long execution time.
+ render() {
+ let {x, y, s, children} = this.props;
+ if (s <= targetSize) {
+ return (
+
+ );
+ return r;
+ }
+ var newSize = s / 2;
+ var slowDown = true;
+ if (slowDown) {
+ var e = performance.now() + 0.8;
+ while (performance.now() < e) {
+ // Artificially long execution time.
+ }
}
- }
- s /= 2;
+ s /= 2;
- return [
-
- {children}
- ,
-
- {children}
- ,
-
- {children}
- ,
- ];
+ return [
+
+ {children}
+ ,
+
+ {children}
+ ,
+
+ {children}
+ ,
+ ];
+ }
}
- SierpinskiTriangle.shouldComponentUpdate = function(oldProps, newProps) {
- var o = oldProps;
- var n = newProps;
- return !(
- o.x === n.x &&
- o.y === n.y &&
- o.s === n.s &&
- o.children === n.children
- );
- };
class ExampleApplication extends React.Component {
constructor() {
diff --git a/src/renderers/art/ReactARTFiberEntry.js b/src/renderers/art/ReactARTFiberEntry.js
index a4e340c2f75e..ca97c71e8d7e 100644
--- a/src/renderers/art/ReactARTFiberEntry.js
+++ b/src/renderers/art/ReactARTFiberEntry.js
@@ -532,6 +532,8 @@ const ARTRenderer = ReactFiberReconciler({
);
},
+ now: ReactDOMFrameScheduling.now,
+
useSyncScheduling: true,
});
diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js
index d0dd466bfb4f..01090873ad72 100644
--- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js
+++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js
@@ -437,6 +437,8 @@ var DOMRenderer = ReactFiberReconciler({
}
},
+ now: ReactDOMFrameScheduling.now,
+
canHydrateInstance(
instance: Instance | TextInstance,
type: string,
diff --git a/src/renderers/native-rt/ReactNativeRTFiberRenderer.js b/src/renderers/native-rt/ReactNativeRTFiberRenderer.js
index 799ee85c1f55..443c84b1fe83 100644
--- a/src/renderers/native-rt/ReactNativeRTFiberRenderer.js
+++ b/src/renderers/native-rt/ReactNativeRTFiberRenderer.js
@@ -227,6 +227,11 @@ const NativeRTRenderer = ReactFiberReconciler({
},
useSyncScheduling: true,
+
+ now(): number {
+ // TODO: Enable expiration by implementing this method.
+ return 0;
+ },
});
module.exports = NativeRTRenderer;
diff --git a/src/renderers/native/ReactNativeFiberRenderer.js b/src/renderers/native/ReactNativeFiberRenderer.js
index 8f36d4886a51..859f422f4c2a 100644
--- a/src/renderers/native/ReactNativeFiberRenderer.js
+++ b/src/renderers/native/ReactNativeFiberRenderer.js
@@ -377,6 +377,11 @@ const NativeRenderer = ReactFiberReconciler({
},
useSyncScheduling: true,
+
+ now(): number {
+ // TODO: Enable expiration by implementing this method.
+ return 0;
+ },
});
module.exports = NativeRenderer;
diff --git a/src/renderers/noop/ReactNoopEntry.js b/src/renderers/noop/ReactNoopEntry.js
index cc98ecb920d9..6a5ed0b11ed3 100644
--- a/src/renderers/noop/ReactNoopEntry.js
+++ b/src/renderers/noop/ReactNoopEntry.js
@@ -83,6 +83,8 @@ function removeChild(
parentInstance.children.splice(index, 1);
}
+let elapsedTimeInMs = 0;
+
var NoopRenderer = ReactFiberReconciler({
getRootHostContext() {
if (failInBeginPhase) {
@@ -201,6 +203,10 @@ var NoopRenderer = ReactFiberReconciler({
prepareForCommit(): void {},
resetAfterCommit(): void {},
+
+ now(): number {
+ return elapsedTimeInMs;
+ },
});
var rootContainers = new Map();
@@ -336,6 +342,14 @@ var ReactNoop = {
expect(actual).toEqual(expected);
},
+ expire(ms: number): void {
+ elapsedTimeInMs += ms;
+ },
+
+ flushExpired(): Array {
+ return ReactNoop.flushUnitsOfWork(0);
+ },
+
yield(value: mixed) {
if (yieldedValues === null) {
yieldedValues = [value];
@@ -400,7 +414,7 @@ var ReactNoop = {
' '.repeat(depth + 1) + '~',
firstUpdate && firstUpdate.partialState,
firstUpdate.callback ? 'with callback' : '',
- '[' + firstUpdate.priorityLevel + ']',
+ '[' + firstUpdate.expirationTime + ']',
);
var next;
while ((next = firstUpdate.next)) {
@@ -408,7 +422,7 @@ var ReactNoop = {
' '.repeat(depth + 1) + '~',
next.partialState,
next.callback ? 'with callback' : '',
- '[' + firstUpdate.priorityLevel + ']',
+ '[' + firstUpdate.expirationTime + ']',
);
}
}
@@ -418,7 +432,7 @@ var ReactNoop = {
' '.repeat(depth) +
'- ' +
(fiber.type ? fiber.type.name || fiber.type : '[root]'),
- '[' + fiber.pendingWorkPriority + (fiber.pendingProps ? '*' : '') + ']',
+ '[' + fiber.expirationTime + (fiber.pendingProps ? '*' : '') + ']',
);
if (fiber.updateQueue) {
logUpdateQueue(fiber.updateQueue, depth);
diff --git a/src/renderers/shared/ReactDOMFrameScheduling.js b/src/renderers/shared/ReactDOMFrameScheduling.js
index de8d0da98f37..5a2fc01ca989 100644
--- a/src/renderers/shared/ReactDOMFrameScheduling.js
+++ b/src/renderers/shared/ReactDOMFrameScheduling.js
@@ -37,6 +37,20 @@ if (__DEV__) {
}
}
+const hasNativePerformanceNow =
+ typeof performance === 'object' && typeof performance.now === 'function';
+
+let now;
+if (hasNativePerformanceNow) {
+ now = function() {
+ return performance.now();
+ };
+} else {
+ now = function() {
+ return Date.now();
+ };
+}
+
// TODO: There's no way to cancel, because Fiber doesn't atm.
let rIC: (callback: (deadline: Deadline) => void) => number;
@@ -67,19 +81,23 @@ if (!ExecutionEnvironment.canUseDOM) {
var previousFrameTime = 33;
var activeFrameTime = 33;
- var frameDeadlineObject = {
- timeRemaining: typeof performance === 'object' &&
- typeof performance.now === 'function'
- ? function() {
- // We assume that if we have a performance timer that the rAF callback
- // gets a performance timer value. Not sure if this is always true.
- return frameDeadline - performance.now();
- }
- : function() {
- // As a fallback we use Date.now.
- return frameDeadline - Date.now();
- },
- };
+ var frameDeadlineObject;
+ if (hasNativePerformanceNow) {
+ frameDeadlineObject = {
+ timeRemaining() {
+ // We assume that if we have a performance timer that the rAF callback
+ // gets a performance timer value. Not sure if this is always true.
+ return frameDeadline - performance.now();
+ },
+ };
+ } else {
+ frameDeadlineObject = {
+ timeRemaining() {
+ // Fallback to Date.now()
+ return frameDeadline - Date.now();
+ },
+ };
+ }
// We use the postMessage trick to defer idle work until after the repaint.
var messageKey = '__reactIdleCallback$' + Math.random().toString(36).slice(2);
@@ -153,4 +171,5 @@ if (!ExecutionEnvironment.canUseDOM) {
rIC = requestIdleCallback;
}
+exports.now = now;
exports.rIC = rIC;
diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js
index 763ee6d9f3c9..935b897b3ca1 100644
--- a/src/renderers/shared/fiber/ReactChildFiber.js
+++ b/src/renderers/shared/fiber/ReactChildFiber.js
@@ -13,7 +13,7 @@
import type {ReactElement} from 'ReactElementType';
import type {ReactCoroutine, ReactPortal, ReactYield} from 'ReactTypes';
import type {Fiber} from 'ReactFiber';
-import type {PriorityLevel} from 'ReactPriorityLevel';
+import type {ExpirationTime} from 'ReactFiberExpirationTime';
var {REACT_COROUTINE_TYPE, REACT_YIELD_TYPE} = require('ReactCoroutine');
var {REACT_PORTAL_TYPE} = require('ReactPortal');
@@ -288,19 +288,19 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
return existingChildren;
}
- function useFiber(fiber: Fiber, priority: PriorityLevel): Fiber {
+ function useFiber(fiber: Fiber, expirationTime: ExpirationTime): Fiber {
// We currently set sibling to null and index to 0 here because it is easy
// to forget to do before returning it. E.g. for the single child case.
if (shouldClone) {
- const clone = createWorkInProgress(fiber, priority);
+ const clone = createWorkInProgress(fiber, expirationTime);
clone.index = 0;
clone.sibling = null;
return clone;
} else {
- // We override the pending priority even if it is higher, because if
- // we're reconciling at a lower priority that means that this was
+ // We override the expiration time even if it is earlier, because if
+ // we're reconciling at a later time that means that this was
// down-prioritized.
- fiber.pendingWorkPriority = priority;
+ fiber.expirationTime = expirationTime;
fiber.effectTag = NoEffect;
fiber.index = 0;
fiber.sibling = null;
@@ -349,20 +349,20 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
current: Fiber | null,
textContent: string,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
) {
if (current === null || current.tag !== HostText) {
// Insert
const created = createFiberFromText(
textContent,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.return = returnFiber;
return created;
} else {
// Update
- const existing = useFiber(current, priority);
+ const existing = useFiber(current, expirationTime);
existing.pendingProps = textContent;
existing.return = returnFiber;
return existing;
@@ -373,21 +373,21 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
current: Fiber | null,
element: ReactElement,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
if (current === null || current.type !== element.type) {
// Insert
const created = createFiberFromElement(
element,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.ref = coerceRef(current, element);
created.return = returnFiber;
return created;
} else {
// Move based on index
- const existing = useFiber(current, priority);
+ const existing = useFiber(current, expirationTime);
existing.ref = coerceRef(current, element);
existing.pendingProps = element.props;
existing.return = returnFiber;
@@ -403,7 +403,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
current: Fiber | null,
coroutine: ReactCoroutine,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
// TODO: Should this also compare handler to determine whether to reuse?
if (current === null || current.tag !== CoroutineComponent) {
@@ -411,13 +411,13 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromCoroutine(
coroutine,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.return = returnFiber;
return created;
} else {
// Move based on index
- const existing = useFiber(current, priority);
+ const existing = useFiber(current, expirationTime);
existing.pendingProps = coroutine;
existing.return = returnFiber;
return existing;
@@ -428,21 +428,21 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
current: Fiber | null,
yieldNode: ReactYield,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
if (current === null || current.tag !== YieldComponent) {
// Insert
const created = createFiberFromYield(
yieldNode,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.type = yieldNode.value;
created.return = returnFiber;
return created;
} else {
// Move based on index
- const existing = useFiber(current, priority);
+ const existing = useFiber(current, expirationTime);
existing.type = yieldNode.value;
existing.return = returnFiber;
return existing;
@@ -453,7 +453,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
current: Fiber | null,
portal: ReactPortal,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
if (
current === null ||
@@ -465,13 +465,13 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromPortal(
portal,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.return = returnFiber;
return created;
} else {
// Update
- const existing = useFiber(current, priority);
+ const existing = useFiber(current, expirationTime);
existing.pendingProps = portal.children || [];
existing.return = returnFiber;
return existing;
@@ -482,20 +482,20 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
current: Fiber | null,
fragment: Iterable<*>,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
if (current === null || current.tag !== Fragment) {
// Insert
const created = createFiberFromFragment(
fragment,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.return = returnFiber;
return created;
} else {
// Update
- const existing = useFiber(current, priority);
+ const existing = useFiber(current, expirationTime);
existing.pendingProps = fragment;
existing.return = returnFiber;
return existing;
@@ -505,7 +505,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
function createChild(
returnFiber: Fiber,
newChild: any,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber | null {
if (typeof newChild === 'string' || typeof newChild === 'number') {
// Text nodes doesn't have keys. If the previous node is implicitly keyed
@@ -514,7 +514,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromText(
'' + newChild,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.return = returnFiber;
return created;
@@ -526,7 +526,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromElement(
newChild,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.ref = coerceRef(null, newChild);
created.return = returnFiber;
@@ -537,7 +537,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromCoroutine(
newChild,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.return = returnFiber;
return created;
@@ -547,7 +547,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromYield(
newChild,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.type = newChild.value;
created.return = returnFiber;
@@ -558,7 +558,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromPortal(
newChild,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.return = returnFiber;
return created;
@@ -569,7 +569,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromFragment(
newChild,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.return = returnFiber;
return created;
@@ -591,7 +591,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
oldFiber: Fiber | null,
newChild: any,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber | null {
// Update the fiber if the keys match, otherwise return null.
@@ -604,14 +604,24 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
if (key !== null) {
return null;
}
- return updateTextNode(returnFiber, oldFiber, '' + newChild, priority);
+ return updateTextNode(
+ returnFiber,
+ oldFiber,
+ '' + newChild,
+ expirationTime,
+ );
}
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
if (newChild.key === key) {
- return updateElement(returnFiber, oldFiber, newChild, priority);
+ return updateElement(
+ returnFiber,
+ oldFiber,
+ newChild,
+ expirationTime,
+ );
} else {
return null;
}
@@ -619,7 +629,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
case REACT_COROUTINE_TYPE: {
if (newChild.key === key) {
- return updateCoroutine(returnFiber, oldFiber, newChild, priority);
+ return updateCoroutine(
+ returnFiber,
+ oldFiber,
+ newChild,
+ expirationTime,
+ );
} else {
return null;
}
@@ -630,7 +645,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
// we can continue to replace it without aborting even if it is not a
// yield.
if (key === null) {
- return updateYield(returnFiber, oldFiber, newChild, priority);
+ return updateYield(returnFiber, oldFiber, newChild, expirationTime);
} else {
return null;
}
@@ -638,7 +653,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
case REACT_PORTAL_TYPE: {
if (newChild.key === key) {
- return updatePortal(returnFiber, oldFiber, newChild, priority);
+ return updatePortal(
+ returnFiber,
+ oldFiber,
+ newChild,
+ expirationTime,
+ );
} else {
return null;
}
@@ -651,7 +671,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
if (key !== null) {
return null;
}
- return updateFragment(returnFiber, oldFiber, newChild, priority);
+ return updateFragment(returnFiber, oldFiber, newChild, expirationTime);
}
throwOnInvalidObjectType(returnFiber, newChild);
@@ -671,13 +691,18 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
newIdx: number,
newChild: any,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber | null {
if (typeof newChild === 'string' || typeof newChild === 'number') {
// Text nodes doesn't have keys, so we neither have to check the old nor
// new node for the key. If both are text nodes, they match.
const matchedFiber = existingChildren.get(newIdx) || null;
- return updateTextNode(returnFiber, matchedFiber, '' + newChild, priority);
+ return updateTextNode(
+ returnFiber,
+ matchedFiber,
+ '' + newChild,
+ expirationTime,
+ );
}
if (typeof newChild === 'object' && newChild !== null) {
@@ -687,7 +712,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
existingChildren.get(
newChild.key === null ? newIdx : newChild.key,
) || null;
- return updateElement(returnFiber, matchedFiber, newChild, priority);
+ return updateElement(
+ returnFiber,
+ matchedFiber,
+ newChild,
+ expirationTime,
+ );
}
case REACT_COROUTINE_TYPE: {
@@ -695,14 +725,24 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
existingChildren.get(
newChild.key === null ? newIdx : newChild.key,
) || null;
- return updateCoroutine(returnFiber, matchedFiber, newChild, priority);
+ return updateCoroutine(
+ returnFiber,
+ matchedFiber,
+ newChild,
+ expirationTime,
+ );
}
case REACT_YIELD_TYPE: {
// Yields doesn't have keys, so we neither have to check the old nor
// new node for the key. If both are yields, they match.
const matchedFiber = existingChildren.get(newIdx) || null;
- return updateYield(returnFiber, matchedFiber, newChild, priority);
+ return updateYield(
+ returnFiber,
+ matchedFiber,
+ newChild,
+ expirationTime,
+ );
}
case REACT_PORTAL_TYPE: {
@@ -710,13 +750,23 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
existingChildren.get(
newChild.key === null ? newIdx : newChild.key,
) || null;
- return updatePortal(returnFiber, matchedFiber, newChild, priority);
+ return updatePortal(
+ returnFiber,
+ matchedFiber,
+ newChild,
+ expirationTime,
+ );
}
}
if (isArray(newChild) || getIteratorFn(newChild)) {
const matchedFiber = existingChildren.get(newIdx) || null;
- return updateFragment(returnFiber, matchedFiber, newChild, priority);
+ return updateFragment(
+ returnFiber,
+ matchedFiber,
+ newChild,
+ expirationTime,
+ );
}
throwOnInvalidObjectType(returnFiber, newChild);
@@ -782,7 +832,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber | null {
// This algorithm can't optimize by searching from boths ends since we
// don't have backpointers on fibers. I'm trying to see how far we can get
@@ -830,7 +880,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber,
oldFiber,
newChildren[newIdx],
- priority,
+ expirationTime,
);
if (newFiber === null) {
// TODO: This breaks on empty slots like null children. That's
@@ -877,7 +927,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const newFiber = createChild(
returnFiber,
newChildren[newIdx],
- priority,
+ expirationTime,
);
if (!newFiber) {
continue;
@@ -904,7 +954,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber,
newIdx,
newChildren[newIdx],
- priority,
+ expirationTime,
);
if (newFiber) {
if (shouldTrackSideEffects) {
@@ -941,7 +991,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildrenIterable: Iterable<*>,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber | null {
// This is the same implementation as reconcileChildrenArray(),
// but using the iterator instead.
@@ -1005,7 +1055,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
} else {
nextOldFiber = oldFiber.sibling;
}
- const newFiber = updateSlot(returnFiber, oldFiber, step.value, priority);
+ const newFiber = updateSlot(
+ returnFiber,
+ oldFiber,
+ step.value,
+ expirationTime,
+ );
if (newFiber === null) {
// TODO: This breaks on empty slots like null children. That's
// unfortunate because it triggers the slow path all the time. We need
@@ -1048,7 +1103,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
// If we don't have any more existing children we can choose a fast path
// since the rest will all be insertions.
for (; !step.done; newIdx++, (step = newChildren.next())) {
- const newFiber = createChild(returnFiber, step.value, priority);
+ const newFiber = createChild(returnFiber, step.value, expirationTime);
if (newFiber === null) {
continue;
}
@@ -1074,7 +1129,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber,
newIdx,
step.value,
- priority,
+ expirationTime,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
@@ -1111,7 +1166,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
textContent: string,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
// There's no need to check for keys on text nodes since we don't have a
// way to define them.
@@ -1119,7 +1174,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
// We already have an existing node so let's just update it and delete
// the rest.
deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
- const existing = useFiber(currentFirstChild, priority);
+ const existing = useFiber(currentFirstChild, expirationTime);
existing.pendingProps = textContent;
existing.return = returnFiber;
return existing;
@@ -1130,7 +1185,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromText(
textContent,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.return = returnFiber;
return created;
@@ -1140,7 +1195,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
const key = element.key;
let child = currentFirstChild;
@@ -1150,7 +1205,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
if (child.key === key) {
if (child.type === element.type) {
deleteRemainingChildren(returnFiber, child.sibling);
- const existing = useFiber(child, priority);
+ const existing = useFiber(child, expirationTime);
existing.ref = coerceRef(child, element);
existing.pendingProps = element.props;
existing.return = returnFiber;
@@ -1172,7 +1227,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromElement(
element,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.ref = coerceRef(currentFirstChild, element);
created.return = returnFiber;
@@ -1183,7 +1238,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
coroutine: ReactCoroutine,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
const key = coroutine.key;
let child = currentFirstChild;
@@ -1193,7 +1248,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
if (child.key === key) {
if (child.tag === CoroutineComponent) {
deleteRemainingChildren(returnFiber, child.sibling);
- const existing = useFiber(child, priority);
+ const existing = useFiber(child, expirationTime);
existing.pendingProps = coroutine;
existing.return = returnFiber;
return existing;
@@ -1210,7 +1265,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromCoroutine(
coroutine,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.return = returnFiber;
return created;
@@ -1220,14 +1275,14 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
yieldNode: ReactYield,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
// There's no need to check for keys on yields since they're stateless.
let child = currentFirstChild;
if (child !== null) {
if (child.tag === YieldComponent) {
deleteRemainingChildren(returnFiber, child.sibling);
- const existing = useFiber(child, priority);
+ const existing = useFiber(child, expirationTime);
existing.type = yieldNode.value;
existing.return = returnFiber;
return existing;
@@ -1239,7 +1294,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromYield(
yieldNode,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.type = yieldNode.value;
created.return = returnFiber;
@@ -1250,7 +1305,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
portal: ReactPortal,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
const key = portal.key;
let child = currentFirstChild;
@@ -1264,7 +1319,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
child.stateNode.implementation === portal.implementation
) {
deleteRemainingChildren(returnFiber, child.sibling);
- const existing = useFiber(child, priority);
+ const existing = useFiber(child, expirationTime);
existing.pendingProps = portal.children || [];
existing.return = returnFiber;
return existing;
@@ -1281,7 +1336,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromPortal(
portal,
returnFiber.internalContextTag,
- priority,
+ expirationTime,
);
created.return = returnFiber;
return created;
@@ -1294,7 +1349,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
- priority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber | null {
// This function is not recursive.
// If the top level item is an array, we treat it as a set of children,
@@ -1304,8 +1359,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
// Handle object types
const isObject = typeof newChild === 'object' && newChild !== null;
if (isObject) {
- // Support only the subset of return types that Stack supports. Treat
- // everything else as empty, but log a warning.
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
@@ -1313,7 +1366,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber,
currentFirstChild,
newChild,
- priority,
+ expirationTime,
),
);
@@ -1323,17 +1376,16 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber,
currentFirstChild,
newChild,
- priority,
+ expirationTime,
),
);
-
case REACT_YIELD_TYPE:
return placeSingleChild(
reconcileSingleYield(
returnFiber,
currentFirstChild,
newChild,
- priority,
+ expirationTime,
),
);
@@ -1343,7 +1395,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber,
currentFirstChild,
newChild,
- priority,
+ expirationTime,
),
);
}
@@ -1355,7 +1407,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber,
currentFirstChild,
'' + newChild,
- priority,
+ expirationTime,
),
);
}
@@ -1365,7 +1417,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber,
currentFirstChild,
newChild,
- priority,
+ expirationTime,
);
}
@@ -1374,7 +1426,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber,
currentFirstChild,
newChild,
- priority,
+ expirationTime,
);
}
@@ -1446,7 +1498,7 @@ exports.cloneChildFibers = function(
let currentChild = workInProgress.child;
let newChild = createWorkInProgress(
currentChild,
- currentChild.pendingWorkPriority,
+ currentChild.expirationTime,
);
// TODO: Pass this as an argument, since it's easy to forget.
newChild.pendingProps = currentChild.pendingProps;
@@ -1457,7 +1509,7 @@ exports.cloneChildFibers = function(
currentChild = currentChild.sibling;
newChild = newChild.sibling = createWorkInProgress(
currentChild,
- currentChild.pendingWorkPriority,
+ currentChild.expirationTime,
);
newChild.pendingProps = currentChild.pendingProps;
newChild.return = workInProgress;
diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js
index dae77d55cc79..8bbffcc48b6b 100644
--- a/src/renderers/shared/fiber/ReactFiber.js
+++ b/src/renderers/shared/fiber/ReactFiber.js
@@ -21,6 +21,7 @@ import type {TypeOfWork} from 'ReactTypeOfWork';
import type {TypeOfInternalContext} from 'ReactTypeOfInternalContext';
import type {TypeOfSideEffect} from 'ReactTypeOfSideEffect';
import type {PriorityLevel} from 'ReactPriorityLevel';
+import type {ExpirationTime} from 'ReactFiberExpirationTime';
import type {UpdateQueue} from 'ReactFiberUpdateQueue';
var {
@@ -35,7 +36,9 @@ var {
Fragment,
} = require('ReactTypeOfWork');
-var {NoWork} = require('ReactPriorityLevel');
+var {NoWork: NoWorkPriority} = require('ReactPriorityLevel');
+
+var {NoWork} = require('ReactFiberExpirationTime');
var {NoContext} = require('ReactTypeOfInternalContext');
@@ -134,8 +137,9 @@ export type Fiber = {|
firstEffect: Fiber | null,
lastEffect: Fiber | null,
- // This will be used to quickly determine if a subtree has no pending changes.
- pendingWorkPriority: PriorityLevel,
+ // Represents a time in the future by which this work should be completed.
+ // This is also used to quickly determine if a subtree has no pending changes.
+ expirationTime: ExpirationTime,
// This is a pooled version of a Fiber. Every fiber that gets updated will
// eventually have a pair. There are cases when we can clean up pairs to save
@@ -189,7 +193,7 @@ function FiberNode(
this.firstEffect = null;
this.lastEffect = null;
- this.pendingWorkPriority = NoWork;
+ this.expirationTime = NoWork;
this.alternate = null;
@@ -233,7 +237,7 @@ function shouldConstruct(Component) {
// This is used to create an alternate fiber to do work on.
exports.createWorkInProgress = function(
current: Fiber,
- renderPriority: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
let workInProgress = current.alternate;
if (workInProgress === null) {
@@ -270,7 +274,7 @@ exports.createWorkInProgress = function(
workInProgress.lastEffect = null;
}
- workInProgress.pendingWorkPriority = renderPriority;
+ workInProgress.expirationTime = expirationTime;
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
@@ -296,7 +300,7 @@ exports.createHostRootFiber = function(): Fiber {
exports.createFiberFromElement = function(
element: ReactElement,
internalContextTag: TypeOfInternalContext,
- priorityLevel: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
let owner = null;
if (__DEV__) {
@@ -310,7 +314,7 @@ exports.createFiberFromElement = function(
owner,
);
fiber.pendingProps = element.props;
- fiber.pendingWorkPriority = priorityLevel;
+ fiber.expirationTime = expirationTime;
if (__DEV__) {
fiber._debugSource = element._source;
@@ -323,24 +327,24 @@ exports.createFiberFromElement = function(
exports.createFiberFromFragment = function(
elements: ReactFragment,
internalContextTag: TypeOfInternalContext,
- priorityLevel: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
// TODO: Consider supporting keyed fragments. Technically, we accidentally
// support that in the existing React.
const fiber = createFiber(Fragment, null, internalContextTag);
fiber.pendingProps = elements;
- fiber.pendingWorkPriority = priorityLevel;
+ fiber.expirationTime = expirationTime;
return fiber;
};
exports.createFiberFromText = function(
content: string,
internalContextTag: TypeOfInternalContext,
- priorityLevel: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
const fiber = createFiber(HostText, null, internalContextTag);
fiber.pendingProps = content;
- fiber.pendingWorkPriority = priorityLevel;
+ fiber.expirationTime = expirationTime;
return fiber;
};
@@ -411,7 +415,7 @@ exports.createFiberFromHostInstanceForDeletion = function(): Fiber {
exports.createFiberFromCoroutine = function(
coroutine: ReactCoroutine,
internalContextTag: TypeOfInternalContext,
- priorityLevel: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
const fiber = createFiber(
CoroutineComponent,
@@ -420,27 +424,28 @@ exports.createFiberFromCoroutine = function(
);
fiber.type = coroutine.handler;
fiber.pendingProps = coroutine;
- fiber.pendingWorkPriority = priorityLevel;
+ fiber.expirationTime = expirationTime;
return fiber;
};
exports.createFiberFromYield = function(
yieldNode: ReactYield,
internalContextTag: TypeOfInternalContext,
- priorityLevel: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
const fiber = createFiber(YieldComponent, null, internalContextTag);
+ fiber.expirationTime = expirationTime;
return fiber;
};
exports.createFiberFromPortal = function(
portal: ReactPortal,
internalContextTag: TypeOfInternalContext,
- priorityLevel: PriorityLevel,
+ expirationTime: ExpirationTime,
): Fiber {
const fiber = createFiber(HostPortal, portal.key, internalContextTag);
fiber.pendingProps = portal.children || [];
- fiber.pendingWorkPriority = priorityLevel;
+ fiber.expirationTime = expirationTime;
fiber.stateNode = {
containerInfo: portal.containerInfo,
implementation: portal.implementation,
@@ -452,5 +457,5 @@ exports.largerPriority = function(
p1: PriorityLevel,
p2: PriorityLevel,
): PriorityLevel {
- return p1 !== NoWork && (p2 === NoWork || p2 > p1) ? p1 : p2;
+ return p1 !== NoWorkPriority && (p2 === NoWorkPriority || p2 > p1) ? p1 : p2;
};
diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js
index ed81a1d77894..0627bc7f9b35 100644
--- a/src/renderers/shared/fiber/ReactFiberBeginWork.js
+++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js
@@ -17,6 +17,7 @@ import type {HydrationContext} from 'ReactFiberHydrationContext';
import type {FiberRoot} from 'ReactFiberRoot';
import type {HostConfig} from 'ReactFiberReconciler';
import type {PriorityLevel} from 'ReactPriorityLevel';
+import type {ExpirationTime} from 'ReactFiberExpirationTime';
var {
mountChildFibersInPlace,
@@ -47,7 +48,7 @@ var {
YieldComponent,
Fragment,
} = ReactTypeOfWork;
-var {NoWork, OffscreenPriority} = require('ReactPriorityLevel');
+var {NoWork, Never} = require('ReactFiberExpirationTime');
var {
PerformedWork,
Placement,
@@ -71,8 +72,16 @@ module.exports = function(
config: HostConfig,
hostContext: HostContext,
hydrationContext: HydrationContext,
- scheduleUpdate: (fiber: Fiber, priorityLevel: PriorityLevel) => void,
- getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel,
+ scheduleUpdate: (fiber: Fiber, expirationTime: ExpirationTime) => void,
+ getPriorityContext: (
+ fiber: Fiber,
+ forceAsync: boolean,
+ ) => PriorityLevel | null,
+ recalculateCurrentTime: () => ExpirationTime,
+ getExpirationTimeForPriority: (
+ currentTime: ExpirationTime,
+ priorityLevel: PriorityLevel | null,
+ ) => ExpirationTime,
) {
const {
shouldSetTextContent,
@@ -99,23 +108,25 @@ module.exports = function(
getPriorityContext,
memoizeProps,
memoizeState,
+ recalculateCurrentTime,
+ getExpirationTimeForPriority,
);
+ // TODO: Remove this and use reconcileChildrenAtExpirationTime directly.
function reconcileChildren(current, workInProgress, nextChildren) {
- const priorityLevel = workInProgress.pendingWorkPriority;
- reconcileChildrenAtPriority(
+ reconcileChildrenAtExpirationTime(
current,
workInProgress,
nextChildren,
- priorityLevel,
+ workInProgress.expirationTime,
);
}
- function reconcileChildrenAtPriority(
+ function reconcileChildrenAtExpirationTime(
current,
workInProgress,
nextChildren,
- priorityLevel,
+ renderExpirationTime,
) {
if (current === null) {
// If this is a fresh new component that hasn't been rendered yet, we
@@ -126,7 +137,7 @@ module.exports = function(
workInProgress,
workInProgress.child,
nextChildren,
- priorityLevel,
+ renderExpirationTime,
);
} else if (current.child === workInProgress.child) {
// If the current child is the same as the work in progress, it means that
@@ -139,7 +150,7 @@ module.exports = function(
workInProgress,
workInProgress.child,
nextChildren,
- priorityLevel,
+ renderExpirationTime,
);
} else {
// If, on the other hand, it is already using a clone, that means we've
@@ -149,7 +160,7 @@ module.exports = function(
workInProgress,
workInProgress.child,
nextChildren,
- priorityLevel,
+ renderExpirationTime,
);
}
}
@@ -223,7 +234,7 @@ module.exports = function(
function updateClassComponent(
current: Fiber | null,
workInProgress: Fiber,
- priorityLevel: PriorityLevel,
+ renderExpirationTime: ExpirationTime,
) {
// Push context providers early to prevent context stack mismatches.
// During mounting we don't know the child context yet as the instance doesn't exist.
@@ -235,18 +246,18 @@ module.exports = function(
if (!workInProgress.stateNode) {
// In the initial pass we might need to construct the instance.
constructClassInstance(workInProgress, workInProgress.pendingProps);
- mountClassInstance(workInProgress, priorityLevel);
+ mountClassInstance(workInProgress, renderExpirationTime);
shouldUpdate = true;
} else {
invariant(false, 'Resuming work not yet implemented.');
// In a resume, we'll already have an instance we can reuse.
- // shouldUpdate = resumeMountClassInstance(workInProgress, priorityLevel);
+ // shouldUpdate = resumeMountClassInstance(workInProgress, renderExpirationTime);
}
} else {
shouldUpdate = updateClassInstance(
current,
workInProgress,
- priorityLevel,
+ renderExpirationTime,
);
}
return finishClassComponent(
@@ -318,7 +329,7 @@ module.exports = function(
pushHostContainer(workInProgress, root.containerInfo);
}
- function updateHostRoot(current, workInProgress, priorityLevel) {
+ function updateHostRoot(current, workInProgress, renderExpirationTime) {
pushHostRootContext(workInProgress);
const updateQueue = workInProgress.updateQueue;
if (updateQueue !== null) {
@@ -330,11 +341,11 @@ module.exports = function(
null,
prevState,
null,
- priorityLevel,
+ renderExpirationTime,
);
if (prevState === state) {
// If the state is the same as before, that's a bailout because we had
- // no work matching this priority.
+ // no work that expires at this time.
resetHydrationState();
return bailoutOnAlreadyFinishedWork(current, workInProgress);
}
@@ -361,7 +372,7 @@ module.exports = function(
workInProgress,
workInProgress.child,
element,
- priorityLevel,
+ renderExpirationTime,
);
} else {
// Otherwise reset hydration state in case we aborted and resumed another
@@ -377,7 +388,7 @@ module.exports = function(
return bailoutOnAlreadyFinishedWork(current, workInProgress);
}
- function updateHostComponent(current, workInProgress, renderPriority) {
+ function updateHostComponent(current, workInProgress, renderExpirationTime) {
pushHostContext(workInProgress);
if (current === null) {
@@ -423,13 +434,13 @@ module.exports = function(
// Check the host config to see if the children are offscreen/hidden.
if (
- renderPriority !== OffscreenPriority &&
+ renderExpirationTime !== Never &&
!useSyncScheduling &&
shouldDeprioritizeSubtree(type, nextProps)
) {
// Down-prioritize the children.
- workInProgress.pendingWorkPriority = OffscreenPriority;
- // Bailout and come back to this fiber later at OffscreenPriority.
+ workInProgress.expirationTime = Never;
+ // Bailout and come back to this fiber later.
return null;
}
@@ -452,7 +463,11 @@ module.exports = function(
return null;
}
- function mountIndeterminateComponent(current, workInProgress, priorityLevel) {
+ function mountIndeterminateComponent(
+ current,
+ workInProgress,
+ renderExpirationTime,
+ ) {
invariant(
current === null,
'An indeterminate component should never have mounted. This error is ' +
@@ -487,7 +502,7 @@ module.exports = function(
// We will invalidate the child context in finishClassComponent() right after rendering.
const hasContext = pushContextProvider(workInProgress);
adoptClassInstance(workInProgress, value);
- mountClassInstance(workInProgress, priorityLevel);
+ mountClassInstance(workInProgress, renderExpirationTime);
return finishClassComponent(current, workInProgress, true, hasContext);
} else {
// Proceed under the assumption that this is a functional component
@@ -532,7 +547,11 @@ module.exports = function(
}
}
- function updateCoroutineComponent(current, workInProgress) {
+ function updateCoroutineComponent(
+ current,
+ workInProgress,
+ renderExpirationTime,
+ ) {
var nextCoroutine = (workInProgress.pendingProps: null | ReactCoroutine);
if (hasContextChanged()) {
// Normally we can bail out on props equality but if context has changed
@@ -556,30 +575,29 @@ module.exports = function(
}
const nextChildren = nextCoroutine.children;
- const priorityLevel = workInProgress.pendingWorkPriority;
- // The following is a fork of reconcileChildrenAtPriority but using
+ // The following is a fork of reconcileChildrenAtExpirationTime but using
// stateNode to store the child.
if (current === null) {
workInProgress.stateNode = mountChildFibersInPlace(
workInProgress,
workInProgress.stateNode,
nextChildren,
- priorityLevel,
+ renderExpirationTime,
);
} else if (current.child === workInProgress.child) {
workInProgress.stateNode = reconcileChildFibers(
workInProgress,
workInProgress.stateNode,
nextChildren,
- priorityLevel,
+ renderExpirationTime,
);
} else {
workInProgress.stateNode = reconcileChildFibersInPlace(
workInProgress,
workInProgress.stateNode,
nextChildren,
- priorityLevel,
+ renderExpirationTime,
);
}
@@ -589,9 +607,12 @@ module.exports = function(
return workInProgress.stateNode;
}
- function updatePortalComponent(current, workInProgress) {
+ function updatePortalComponent(
+ current,
+ workInProgress,
+ renderExpirationTime,
+ ) {
pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo);
- const priorityLevel = workInProgress.pendingWorkPriority;
let nextChildren = workInProgress.pendingProps;
if (hasContextChanged()) {
// Normally we can bail out on props equality but if context has changed
@@ -621,7 +642,7 @@ module.exports = function(
workInProgress,
workInProgress.child,
nextChildren,
- priorityLevel,
+ renderExpirationTime,
);
memoizeProps(workInProgress, nextChildren);
} else {
@@ -716,11 +737,11 @@ module.exports = function(
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
- priorityLevel: PriorityLevel,
+ renderExpirationTime: ExpirationTime,
): Fiber | null {
if (
- workInProgress.pendingWorkPriority === NoWork ||
- workInProgress.pendingWorkPriority > priorityLevel
+ workInProgress.expirationTime === NoWork ||
+ workInProgress.expirationTime > renderExpirationTime
) {
return bailoutOnLowPriority(current, workInProgress);
}
@@ -730,16 +751,24 @@ module.exports = function(
return mountIndeterminateComponent(
current,
workInProgress,
- priorityLevel,
+ renderExpirationTime,
);
case FunctionalComponent:
return updateFunctionalComponent(current, workInProgress);
case ClassComponent:
- return updateClassComponent(current, workInProgress, priorityLevel);
+ return updateClassComponent(
+ current,
+ workInProgress,
+ renderExpirationTime,
+ );
case HostRoot:
- return updateHostRoot(current, workInProgress, priorityLevel);
+ return updateHostRoot(current, workInProgress, renderExpirationTime);
case HostComponent:
- return updateHostComponent(current, workInProgress, priorityLevel);
+ return updateHostComponent(
+ current,
+ workInProgress,
+ renderExpirationTime,
+ );
case HostText:
return updateHostText(current, workInProgress);
case CoroutineHandlerPhase:
@@ -747,13 +776,21 @@ module.exports = function(
workInProgress.tag = CoroutineComponent;
// Intentionally fall through since this is now the same.
case CoroutineComponent:
- return updateCoroutineComponent(current, workInProgress);
+ return updateCoroutineComponent(
+ current,
+ workInProgress,
+ renderExpirationTime,
+ );
case YieldComponent:
// A yield component is just a placeholder, we can just run through the
// next one immediately.
return null;
case HostPortal:
- return updatePortalComponent(current, workInProgress);
+ return updatePortalComponent(
+ current,
+ workInProgress,
+ renderExpirationTime,
+ );
case Fragment:
return updateFragment(current, workInProgress);
default:
@@ -768,7 +805,7 @@ module.exports = function(
function beginFailedWork(
current: Fiber | null,
workInProgress: Fiber,
- priorityLevel: PriorityLevel,
+ renderExpirationTime: ExpirationTime,
) {
// Push context providers here to avoid a push/pop context mismatch.
switch (workInProgress.tag) {
@@ -801,8 +838,8 @@ module.exports = function(
}
if (
- workInProgress.pendingWorkPriority === NoWork ||
- workInProgress.pendingWorkPriority > priorityLevel
+ workInProgress.expirationTime === NoWork ||
+ workInProgress.expirationTime > renderExpirationTime
) {
return bailoutOnLowPriority(current, workInProgress);
}
@@ -814,11 +851,11 @@ module.exports = function(
// Unmount the current children as if the component rendered null
const nextChildren = null;
- reconcileChildrenAtPriority(
+ reconcileChildrenAtExpirationTime(
current,
workInProgress,
nextChildren,
- priorityLevel,
+ renderExpirationTime,
);
if (workInProgress.tag === ClassComponent) {
diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js
index 2e080e23bff4..62f35742b025 100644
--- a/src/renderers/shared/fiber/ReactFiberClassComponent.js
+++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js
@@ -12,6 +12,7 @@
import type {Fiber} from 'ReactFiber';
import type {PriorityLevel} from 'ReactPriorityLevel';
+import type {ExpirationTime} from 'ReactFiberExpirationTime';
var {Update} = require('ReactTypeOfSideEffect');
@@ -77,10 +78,18 @@ if (__DEV__) {
}
module.exports = function(
- scheduleUpdate: (fiber: Fiber, priorityLevel: PriorityLevel) => void,
- getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel,
+ scheduleUpdate: (fiber: Fiber, expirationTime: ExpirationTime) => void,
+ getPriorityContext: (
+ fiber: Fiber,
+ forceAsync: boolean,
+ ) => PriorityLevel | null,
memoizeProps: (workInProgress: Fiber, props: any) => void,
memoizeState: (workInProgress: Fiber, state: any) => void,
+ recalculateCurrentTime: () => ExpirationTime,
+ getExpirationTimeForPriority: (
+ currentTime: ExpirationTime,
+ priorityLevel: PriorityLevel | null,
+ ) => ExpirationTime,
) {
// Class component state updater
const updater = {
@@ -88,32 +97,67 @@ module.exports = function(
enqueueSetState(instance, partialState, callback) {
const fiber = ReactInstanceMap.get(instance);
const priorityLevel = getPriorityContext(fiber, false);
+ const currentTime = recalculateCurrentTime();
+ const expirationTime = getExpirationTimeForPriority(
+ currentTime,
+ priorityLevel,
+ );
callback = callback === undefined ? null : callback;
if (__DEV__) {
warnOnInvalidCallback(callback, 'setState');
}
- addUpdate(fiber, partialState, callback, priorityLevel);
- scheduleUpdate(fiber, priorityLevel);
+ addUpdate(
+ fiber,
+ partialState,
+ callback,
+ priorityLevel,
+ expirationTime,
+ currentTime,
+ );
+ scheduleUpdate(fiber, expirationTime);
},
enqueueReplaceState(instance, state, callback) {
const fiber = ReactInstanceMap.get(instance);
const priorityLevel = getPriorityContext(fiber, false);
+ const currentTime = recalculateCurrentTime();
+ const expirationTime = getExpirationTimeForPriority(
+ currentTime,
+ priorityLevel,
+ );
callback = callback === undefined ? null : callback;
if (__DEV__) {
warnOnInvalidCallback(callback, 'replaceState');
}
- addReplaceUpdate(fiber, state, callback, priorityLevel);
- scheduleUpdate(fiber, priorityLevel);
+ addReplaceUpdate(
+ fiber,
+ state,
+ callback,
+ priorityLevel,
+ expirationTime,
+ currentTime,
+ );
+ scheduleUpdate(fiber, expirationTime);
},
enqueueForceUpdate(instance, callback) {
const fiber = ReactInstanceMap.get(instance);
const priorityLevel = getPriorityContext(fiber, false);
+ const currentTime = recalculateCurrentTime();
+ const expirationTime = getExpirationTimeForPriority(
+ currentTime,
+ priorityLevel,
+ );
callback = callback === undefined ? null : callback;
if (__DEV__) {
warnOnInvalidCallback(callback, 'forceUpdate');
}
- addForceUpdate(fiber, callback, priorityLevel);
- scheduleUpdate(fiber, priorityLevel);
+ addForceUpdate(
+ fiber,
+ callback,
+ priorityLevel,
+ expirationTime,
+ currentTime,
+ );
+ scheduleUpdate(fiber, expirationTime);
},
};
@@ -383,7 +427,7 @@ module.exports = function(
// Invokes the mount life-cycles on a previously never rendered instance.
function mountClassInstance(
workInProgress: Fiber,
- priorityLevel: PriorityLevel,
+ renderExpirationTime: ExpirationTime,
): void {
const current = workInProgress.alternate;
@@ -430,7 +474,7 @@ module.exports = function(
instance,
state,
props,
- priorityLevel,
+ renderExpirationTime,
);
}
}
@@ -548,7 +592,7 @@ module.exports = function(
function updateClassInstance(
current: Fiber,
workInProgress: Fiber,
- priorityLevel: PriorityLevel,
+ renderExpirationTime: ExpirationTime,
): boolean {
const instance = workInProgress.stateNode;
resetInputPointers(workInProgress, instance);
@@ -597,7 +641,7 @@ module.exports = function(
instance,
oldState,
newProps,
- priorityLevel,
+ renderExpirationTime,
);
} else {
newState = oldState;
diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js
index 451e379965da..edb6048e9133 100644
--- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js
+++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js
@@ -12,7 +12,7 @@
import type {ReactCoroutine} from 'ReactTypes';
import type {Fiber} from 'ReactFiber';
-import type {PriorityLevel} from 'ReactPriorityLevel';
+import type {ExpirationTime} from 'ReactFiberExpirationTime';
import type {HostContext} from 'ReactFiberHostContext';
import type {HydrationContext} from 'ReactFiberHydrationContext';
import type {FiberRoot} from 'ReactFiberRoot';
@@ -25,7 +25,7 @@ var {
} = require('ReactFiberContext');
var ReactTypeOfWork = require('ReactTypeOfWork');
var ReactTypeOfSideEffect = require('ReactTypeOfSideEffect');
-var ReactPriorityLevel = require('ReactPriorityLevel');
+var ReactFiberExpirationTime = require('ReactFiberExpirationTime');
var {
IndeterminateComponent,
FunctionalComponent,
@@ -40,7 +40,7 @@ var {
Fragment,
} = ReactTypeOfWork;
var {Placement, Ref, Update} = ReactTypeOfSideEffect;
-var {OffscreenPriority} = ReactPriorityLevel;
+var {Never} = ReactFiberExpirationTime;
var invariant = require('fbjs/lib/invariant');
@@ -113,6 +113,7 @@ module.exports = function(
function moveCoroutineToHandlerPhase(
current: Fiber | null,
workInProgress: Fiber,
+ renderExpirationTime: ExpirationTime,
) {
var coroutine = (workInProgress.memoizedProps: ?ReactCoroutine);
invariant(
@@ -139,13 +140,11 @@ module.exports = function(
var nextChildren = fn(props, yields);
var currentFirstChild = current !== null ? current.child : null;
- // Inherit the priority of the returnFiber.
- const priority = workInProgress.pendingWorkPriority;
workInProgress.child = reconcileChildFibers(
workInProgress,
currentFirstChild,
nextChildren,
- priority,
+ renderExpirationTime,
);
return workInProgress.child;
}
@@ -181,15 +180,15 @@ module.exports = function(
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
- renderPriority: PriorityLevel,
+ renderExpirationTime: ExpirationTime,
): Fiber | null {
// Get the latest props.
let newProps = workInProgress.pendingProps;
if (newProps === null) {
newProps = workInProgress.memoizedProps;
} else if (
- workInProgress.pendingWorkPriority !== OffscreenPriority ||
- renderPriority === OffscreenPriority
+ workInProgress.expirationTime !== Never ||
+ renderExpirationTime === Never
) {
// Reset the pending props, unless this was a down-prioritization.
workInProgress.pendingProps = null;
@@ -358,7 +357,11 @@ module.exports = function(
return null;
}
case CoroutineComponent:
- return moveCoroutineToHandlerPhase(current, workInProgress);
+ return moveCoroutineToHandlerPhase(
+ current,
+ workInProgress,
+ renderExpirationTime,
+ );
case CoroutineHandlerPhase:
// Reset the tag to now be a first phase coroutine.
workInProgress.tag = CoroutineComponent;
diff --git a/src/renderers/shared/fiber/ReactFiberExpirationTime.js b/src/renderers/shared/fiber/ReactFiberExpirationTime.js
new file mode 100644
index 000000000000..0413f8a5994d
--- /dev/null
+++ b/src/renderers/shared/fiber/ReactFiberExpirationTime.js
@@ -0,0 +1,123 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @providesModule ReactFiberExpirationTime
+ * @flow
+ */
+
+'use strict';
+
+import type {PriorityLevel} from 'ReactPriorityLevel';
+const {
+ NoWork: NoWorkPriority,
+ SynchronousPriority,
+ TaskPriority,
+ HighPriority,
+ LowPriority,
+ OffscreenPriority,
+} = require('ReactPriorityLevel');
+
+const invariant = require('fbjs/lib/invariant');
+
+// TODO: Use an opaque type once ESLint et al support the syntax
+export type ExpirationTime = number;
+
+const NoWork = 0;
+const Sync = 1;
+const Task = 2;
+const Never = 2147483647; // Max int32: Math.pow(2, 31) - 1
+
+const UNIT_SIZE = 10;
+const MAGIC_NUMBER_OFFSET = 3;
+
+exports.NoWork = NoWork;
+exports.Never = Never;
+
+// 1 unit of expiration time represents 10ms.
+function msToExpirationTime(ms: number): ExpirationTime {
+ // Always add an offset so that we don't clash with the magic number for NoWork.
+ return ((ms / UNIT_SIZE) | 0) + MAGIC_NUMBER_OFFSET;
+}
+exports.msToExpirationTime = msToExpirationTime;
+
+function ceiling(num: number, precision: number): number {
+ return (((((num * precision) | 0) + 1) / precision) | 0) + 1;
+}
+
+function bucket(
+ currentTime: ExpirationTime,
+ expirationInMs: number,
+ precisionInMs: number,
+): ExpirationTime {
+ return ceiling(
+ currentTime + expirationInMs / UNIT_SIZE,
+ precisionInMs / UNIT_SIZE,
+ );
+}
+
+// Given the current clock time and a priority level, returns an expiration time
+// that represents a point in the future by which some work should complete.
+// The lower the priority, the further out the expiration time. We use rounding
+// to batch like updates together. The further out the expiration time, the
+// more we want to batch, so we use a larger precision when rounding.
+function priorityToExpirationTime(
+ currentTime: ExpirationTime,
+ priorityLevel: PriorityLevel,
+): ExpirationTime {
+ switch (priorityLevel) {
+ case NoWork:
+ return NoWorkPriority;
+ case SynchronousPriority:
+ return Sync;
+ case TaskPriority:
+ return Task;
+ case HighPriority: {
+ // Should complete within ~100ms. 120ms max.
+ return bucket(currentTime, 100, 20);
+ }
+ case LowPriority: {
+ // Should complete within ~1000ms. 1200ms max.
+ return bucket(currentTime, 1000, 200);
+ }
+ case OffscreenPriority:
+ return Never;
+ default:
+ invariant(
+ false,
+ 'Switch statement should be exhuastive. ' +
+ 'This error is likely caused by a bug in React. Please file an issue.',
+ );
+ }
+}
+exports.priorityToExpirationTime = priorityToExpirationTime;
+
+// Given the current clock time and an expiration time, returns the
+// corresponding priority level. The more time has advanced, the higher the
+// priority level.
+function expirationTimeToPriorityLevel(
+ currentTime: ExpirationTime,
+ expirationTime: ExpirationTime,
+): PriorityLevel {
+ // First check for magic values
+ switch (expirationTime) {
+ case NoWorkPriority:
+ return NoWork;
+ case Sync:
+ return SynchronousPriority;
+ case Task:
+ return TaskPriority;
+ case Never:
+ return OffscreenPriority;
+ default:
+ break;
+ }
+ if (expirationTime <= currentTime) {
+ return TaskPriority;
+ }
+ // TODO: We don't currently distinguish between high and low priority.
+ return LowPriority;
+}
+exports.expirationTimeToPriorityLevel = expirationTimeToPriorityLevel;
diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js
index 4876d88f4607..c672fe86a5c5 100644
--- a/src/renderers/shared/fiber/ReactFiberReconciler.js
+++ b/src/renderers/shared/fiber/ReactFiberReconciler.js
@@ -122,6 +122,8 @@ export type HostConfig = {
prepareForCommit(): void,
resetAfterCommit(): void,
+ now(): number,
+
// Optional hydration
canHydrateInstance?: (instance: I | TI, type: T, props: P) => boolean,
canHydrateTextInstance?: (instance: I | TI, text: string) => boolean,
@@ -235,6 +237,8 @@ module.exports = function(
var {
scheduleUpdate,
getPriorityContext,
+ getExpirationTimeForPriority,
+ recalculateCurrentTime,
batchedUpdates,
unbatchedUpdates,
flushSync,
@@ -274,6 +278,11 @@ module.exports = function(
element.type.prototype != null &&
(element.type.prototype: any).unstable_isAsyncReactComponent === true;
const priorityLevel = getPriorityContext(current, forceAsync);
+ const currentTime = recalculateCurrentTime();
+ const expirationTime = getExpirationTimeForPriority(
+ currentTime,
+ priorityLevel,
+ );
const nextState = {element};
callback = callback === undefined ? null : callback;
if (__DEV__) {
@@ -284,8 +293,15 @@ module.exports = function(
callback,
);
}
- addTopLevelUpdate(current, nextState, callback, priorityLevel);
- scheduleUpdate(current, priorityLevel);
+ addTopLevelUpdate(
+ current,
+ nextState,
+ callback,
+ priorityLevel,
+ expirationTime,
+ currentTime,
+ );
+ scheduleUpdate(current, expirationTime);
}
return {
diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js
index cc3800d129ea..52e6ebee2627 100644
--- a/src/renderers/shared/fiber/ReactFiberScheduler.js
+++ b/src/renderers/shared/fiber/ReactFiberScheduler.js
@@ -15,6 +15,7 @@ import type {FiberRoot} from 'ReactFiberRoot';
import type {HostConfig, Deadline} from 'ReactFiberReconciler';
import type {PriorityLevel} from 'ReactPriorityLevel';
import type {HydrationContext} from 'ReactFiberHydrationContext';
+import type {ExpirationTime} from 'ReactFiberExpirationTime';
export type CapturedError = {
componentName: ?string,
@@ -50,7 +51,7 @@ var ReactFiberHydrationContext = require('ReactFiberHydrationContext');
var {ReactCurrentOwner} = require('ReactGlobalSharedState');
var getComponentName = require('getComponentName');
-var {createWorkInProgress, largerPriority} = require('ReactFiber');
+var {createWorkInProgress} = require('ReactFiber');
var {onCommitRoot} = require('ReactFiberDevToolsHook');
var {
@@ -62,6 +63,13 @@ var {
OffscreenPriority,
} = require('ReactPriorityLevel');
+var {
+ Never,
+ msToExpirationTime,
+ priorityToExpirationTime,
+ expirationTimeToPriorityLevel,
+} = require('ReactFiberExpirationTime');
+
var {AsyncUpdates} = require('ReactTypeOfInternalContext');
var {
@@ -83,7 +91,7 @@ var {
ClassComponent,
} = require('ReactTypeOfWork');
-var {getUpdatePriority} = require('ReactFiberUpdateQueue');
+var {getUpdateExpirationTime} = require('ReactFiberUpdateQueue');
var {resetContext} = require('ReactFiberContext');
@@ -166,6 +174,8 @@ module.exports = function(
hydrationContext,
scheduleUpdate,
getPriorityContext,
+ recalculateCurrentTime,
+ getExpirationTimeForPriority,
);
const {completeWork} = ReactFiberCompleteWork(
config,
@@ -181,16 +191,19 @@ module.exports = function(
commitDetachRef,
} = ReactFiberCommitWork(config, captureError);
const {
+ now,
scheduleDeferredCallback,
useSyncScheduling,
prepareForCommit,
resetAfterCommit,
} = config;
+ // Represents the current time in ms.
+ const startTime = now();
+ let mostRecentCurrentTime: ExpirationTime = msToExpirationTime(0);
+
// The priority level to use when scheduling an update. We use NoWork to
// represent the default priority.
- // TODO: Should we change this to an array instead of using the call stack?
- // Might be less confusing.
let priorityContext: PriorityLevel = NoWork;
// Keeps track of whether we're currently in a work loop.
@@ -208,7 +221,8 @@ module.exports = function(
// The next work in progress fiber that we're currently working on.
let nextUnitOfWork: Fiber | null = null;
- let nextPriorityLevel: PriorityLevel = NoWork;
+ // The time at which we're currently rendering work.
+ let nextRenderExpirationTime: ExpirationTime = NoWork;
// The next fiber with an effect that we're currently committing.
let nextEffect: Fiber | null = null;
@@ -251,14 +265,11 @@ module.exports = function(
resetHostContainer();
}
- // resetNextUnitOfWork mutates the current priority context. It is reset after
- // after the workLoop exits, so never call resetNextUnitOfWork from outside
- // the work loop.
function resetNextUnitOfWork() {
// Clear out roots with no more work on them, or if they have uncaught errors
while (
nextScheduledRoot !== null &&
- nextScheduledRoot.current.pendingWorkPriority === NoWork
+ nextScheduledRoot.current.expirationTime === NoWork
) {
// Unschedule this root.
nextScheduledRoot.isScheduled = false;
@@ -270,7 +281,7 @@ module.exports = function(
if (nextScheduledRoot === lastScheduledRoot) {
nextScheduledRoot = null;
lastScheduledRoot = null;
- nextPriorityLevel = NoWork;
+ nextRenderExpirationTime = NoWork;
return null;
}
// Continue with the next root.
@@ -279,22 +290,22 @@ module.exports = function(
}
let root = nextScheduledRoot;
- let highestPriorityRoot = null;
- let highestPriorityLevel = NoWork;
+ let earliestExpirationRoot = null;
+ let earliestExpirationTime = NoWork;
while (root !== null) {
if (
- root.current.pendingWorkPriority !== NoWork &&
- (highestPriorityLevel === NoWork ||
- highestPriorityLevel > root.current.pendingWorkPriority)
+ root.current.expirationTime !== NoWork &&
+ (earliestExpirationTime === NoWork ||
+ earliestExpirationTime > root.current.expirationTime)
) {
- highestPriorityLevel = root.current.pendingWorkPriority;
- highestPriorityRoot = root;
+ earliestExpirationTime = root.current.expirationTime;
+ earliestExpirationRoot = root;
}
// We didn't find anything to do in this root, so let's try the next one.
root = root.nextScheduledRoot;
}
- if (highestPriorityRoot !== null) {
- nextPriorityLevel = highestPriorityLevel;
+ if (earliestExpirationRoot !== null) {
+ nextRenderExpirationTime = earliestExpirationTime;
// Before we start any new work, let's make sure that we have a fresh
// stack to work from.
// TODO: This call is buried a bit too deep. It would be nice to have
@@ -303,18 +314,18 @@ module.exports = function(
resetContextStack();
nextUnitOfWork = createWorkInProgress(
- highestPriorityRoot.current,
- highestPriorityLevel,
+ earliestExpirationRoot.current,
+ earliestExpirationTime,
);
- if (highestPriorityRoot !== nextRenderedTree) {
+ if (earliestExpirationRoot !== nextRenderedTree) {
// We've switched trees. Reset the nested update counter.
nestedUpdateCount = 0;
- nextRenderedTree = highestPriorityRoot;
+ nextRenderedTree = earliestExpirationRoot;
}
return;
}
- nextPriorityLevel = NoWork;
+ nextRenderExpirationTime = NoWork;
nextUnitOfWork = null;
nextRenderedTree = null;
return;
@@ -392,7 +403,6 @@ module.exports = function(
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
- // Use Task priority for lifecycle updates
if (effectTag & (Update | Callback)) {
if (__DEV__) {
recordEffect();
@@ -446,10 +456,7 @@ module.exports = function(
'in React. Please file an issue.',
);
- if (
- nextPriorityLevel === SynchronousPriority ||
- nextPriorityLevel === TaskPriority
- ) {
+ if (nextRenderExpirationTime <= mostRecentCurrentTime) {
// Keep track of the number of iterations to prevent an infinite
// update loop.
nestedUpdateCount++;
@@ -583,35 +590,39 @@ module.exports = function(
commitPhaseBoundaries = null;
}
- // This tree is done. Reset the unit of work pointer to the next highest
- // priority root. If there's no more work left, the pointer is set to null.
+ // This tree is done. Reset the unit of work pointer to the root that
+ // expires soonest. If there's no work left, the pointer is set to null.
resetNextUnitOfWork();
}
- function resetWorkPriority(
+ function resetExpirationTime(
workInProgress: Fiber,
- renderPriority: PriorityLevel,
+ renderTime: ExpirationTime,
) {
- if (
- workInProgress.pendingWorkPriority !== NoWork &&
- workInProgress.pendingWorkPriority > renderPriority
- ) {
- // This was a down-prioritization. Don't bubble priority from children.
+ if (renderTime !== Never && workInProgress.expirationTime === Never) {
+ // The children of this component are hidden. Don't bubble their
+ // expiration times.
return;
}
- // Check for pending update priority.
- let newPriority = getUpdatePriority(workInProgress);
+ // Check for pending updates.
+ let newExpirationTime = getUpdateExpirationTime(workInProgress);
// TODO: Coroutines need to visit stateNode
+ // Bubble up the earliest expiration time.
let child = workInProgress.child;
while (child !== null) {
- // Ensure that remaining work priority bubbles up.
- newPriority = largerPriority(newPriority, child.pendingWorkPriority);
+ if (
+ child.expirationTime !== NoWork &&
+ (newExpirationTime === NoWork ||
+ newExpirationTime > child.expirationTime)
+ ) {
+ newExpirationTime = child.expirationTime;
+ }
child = child.sibling;
}
- workInProgress.pendingWorkPriority = newPriority;
+ workInProgress.expirationTime = newExpirationTime;
}
function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
@@ -624,7 +635,11 @@ module.exports = function(
if (__DEV__) {
ReactDebugCurrentFiber.setCurrentFiber(workInProgress);
}
- const next = completeWork(current, workInProgress, nextPriorityLevel);
+ const next = completeWork(
+ current,
+ workInProgress,
+ nextRenderExpirationTime,
+ );
if (__DEV__) {
ReactDebugCurrentFiber.resetCurrentFiber();
}
@@ -632,7 +647,7 @@ module.exports = function(
const returnFiber = workInProgress.return;
const siblingFiber = workInProgress.sibling;
- resetWorkPriority(workInProgress, nextPriorityLevel);
+ resetExpirationTime(workInProgress, nextRenderExpirationTime);
if (next !== null) {
if (__DEV__) {
@@ -720,7 +735,7 @@ module.exports = function(
startWorkTimer(workInProgress);
ReactDebugCurrentFiber.setCurrentFiber(workInProgress);
}
- let next = beginWork(current, workInProgress, nextPriorityLevel);
+ let next = beginWork(current, workInProgress, nextRenderExpirationTime);
if (__DEV__) {
ReactDebugCurrentFiber.resetCurrentFiber();
}
@@ -750,7 +765,11 @@ module.exports = function(
startWorkTimer(workInProgress);
ReactDebugCurrentFiber.setCurrentFiber(workInProgress);
}
- let next = beginFailedWork(current, workInProgress, nextPriorityLevel);
+ let next = beginFailedWork(
+ current,
+ workInProgress,
+ nextRenderExpirationTime,
+ );
if (__DEV__) {
ReactDebugCurrentFiber.resetCurrentFiber();
}
@@ -785,7 +804,8 @@ module.exports = function(
if (
capturedErrors !== null &&
capturedErrors.size > 0 &&
- nextPriorityLevel === TaskPriority
+ nextRenderExpirationTime !== NoWork &&
+ nextRenderExpirationTime <= mostRecentCurrentTime
) {
while (nextUnitOfWork !== null) {
if (hasCapturedError(nextUnitOfWork)) {
@@ -801,14 +821,12 @@ module.exports = function(
'a bug in React. Please file an issue.',
);
// We just completed a root. Commit it now.
- priorityContext = TaskPriority;
commitAllWork(pendingCommit);
- priorityContext = nextPriorityLevel;
-
if (
capturedErrors === null ||
capturedErrors.size === 0 ||
- nextPriorityLevel !== TaskPriority
+ nextRenderExpirationTime === NoWork ||
+ nextRenderExpirationTime > mostRecentCurrentTime
) {
// There are no more unhandled errors. We can exit this special
// work loop. If there's still additional work, we'll perform it
@@ -822,28 +840,26 @@ module.exports = function(
}
function workLoop(
- minPriorityLevel: PriorityLevel,
+ minExpirationTime: ExpirationTime,
deadline: Deadline | null,
) {
if (pendingCommit !== null) {
- priorityContext = TaskPriority;
commitAllWork(pendingCommit);
handleCommitPhaseErrors();
} else if (nextUnitOfWork === null) {
resetNextUnitOfWork();
}
- if (nextPriorityLevel === NoWork || nextPriorityLevel > minPriorityLevel) {
+ if (
+ nextRenderExpirationTime === NoWork ||
+ nextRenderExpirationTime > minExpirationTime
+ ) {
return;
}
- // During the render phase, updates should have the same priority at which
- // we're rendering.
- priorityContext = nextPriorityLevel;
-
loop: do {
- if (nextPriorityLevel <= TaskPriority) {
- // Flush all synchronous and task work.
+ if (nextRenderExpirationTime <= mostRecentCurrentTime) {
+ // Flush all expired work.
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
if (nextUnitOfWork === null) {
@@ -853,24 +869,22 @@ module.exports = function(
'a bug in React. Please file an issue.',
);
// We just completed a root. Commit it now.
- priorityContext = TaskPriority;
commitAllWork(pendingCommit);
- priorityContext = nextPriorityLevel;
// Clear any errors that were scheduled during the commit phase.
handleCommitPhaseErrors();
- // The priority level may have changed. Check again.
+ // The render time may have changed. Check again.
if (
- nextPriorityLevel === NoWork ||
- nextPriorityLevel > minPriorityLevel ||
- nextPriorityLevel > TaskPriority
+ nextRenderExpirationTime === NoWork ||
+ nextRenderExpirationTime > minExpirationTime ||
+ nextRenderExpirationTime > mostRecentCurrentTime
) {
- // The priority level does not match.
+ // We've completed all the expired work.
break;
}
}
}
} else if (deadline !== null) {
- // Flush asynchronous work until the deadline expires.
+ // Flush asynchronous work until the deadline runs out of time.
while (nextUnitOfWork !== null && !deadlineHasExpired) {
if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
@@ -887,18 +901,16 @@ module.exports = function(
// We just completed a root. If we have time, commit it now.
// Otherwise, we'll commit it in the next frame.
if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
- priorityContext = TaskPriority;
commitAllWork(pendingCommit);
- priorityContext = nextPriorityLevel;
// Clear any errors that were scheduled during the commit phase.
handleCommitPhaseErrors();
- // The priority level may have changed. Check again.
+ // The render time may have changed. Check again.
if (
- nextPriorityLevel === NoWork ||
- nextPriorityLevel > minPriorityLevel ||
- nextPriorityLevel < HighPriority
+ nextRenderExpirationTime === NoWork ||
+ nextRenderExpirationTime > minExpirationTime ||
+ nextRenderExpirationTime <= mostRecentCurrentTime
) {
- // The priority level does not match.
+ // We've completed all the async work.
break;
}
} else {
@@ -913,12 +925,16 @@ module.exports = function(
// There might be work left. Depending on the priority, we should
// either perform it now or schedule a callback to perform it later.
- switch (nextPriorityLevel) {
+ const currentTime = recalculateCurrentTime();
+ switch (expirationTimeToPriorityLevel(
+ currentTime,
+ nextRenderExpirationTime,
+ )) {
case SynchronousPriority:
case TaskPriority:
// We have remaining synchronous or task work. Keep performing it,
// regardless of whether we're inside a callback.
- if (nextPriorityLevel <= minPriorityLevel) {
+ if (nextRenderExpirationTime <= minExpirationTime) {
continue loop;
}
break loop;
@@ -932,7 +948,10 @@ module.exports = function(
break loop;
}
// We are inside a callback.
- if (!deadlineHasExpired && nextPriorityLevel <= minPriorityLevel) {
+ if (
+ !deadlineHasExpired &&
+ nextRenderExpirationTime <= minExpirationTime
+ ) {
// We still have time. Keep working.
continue loop;
}
@@ -954,7 +973,7 @@ module.exports = function(
function performWorkCatchBlock(
failedWork: Fiber,
boundary: Fiber,
- minPriorityLevel: PriorityLevel,
+ minExpirationTime: ExpirationTime,
deadline: Deadline | null,
) {
// We're going to restart the error boundary that captured the error.
@@ -970,7 +989,7 @@ module.exports = function(
nextUnitOfWork = performFailedUnitOfWork(boundary);
// Continue working.
- workLoop(minPriorityLevel, deadline);
+ workLoop(minExpirationTime, deadline);
}
function performWork(
@@ -988,21 +1007,32 @@ module.exports = function(
);
isPerformingWork = true;
- // The priority context changes during the render phase. We'll need to
- // reset it at the end.
+ // Updates that occur during the commit phase should have task priority
+ // by default. (Render phase updates are special; getPriorityContext
+ // accounts for their behavior.)
const previousPriorityContext = priorityContext;
+ priorityContext = TaskPriority;
+
+ // Read the current time from the host environment.
+ const currentTime = recalculateCurrentTime();
+ const minExpirationTime = getExpirationTimeForPriority(
+ currentTime,
+ minPriorityLevel,
+ );
+
+ nestedUpdateCount = 0;
let didError = false;
let error = null;
if (__DEV__) {
- invokeGuardedCallback(null, workLoop, null, minPriorityLevel, deadline);
+ invokeGuardedCallback(null, workLoop, null, minExpirationTime, deadline);
if (hasCaughtError()) {
didError = true;
error = clearCaughtError();
}
} else {
try {
- workLoop(minPriorityLevel, deadline);
+ workLoop(minExpirationTime, deadline);
} catch (e) {
didError = true;
error = e;
@@ -1049,7 +1079,7 @@ module.exports = function(
null,
failedWork,
boundary,
- minPriorityLevel,
+ minExpirationTime,
deadline,
);
if (hasCaughtError()) {
@@ -1062,7 +1092,7 @@ module.exports = function(
performWorkCatchBlock(
failedWork,
boundary,
- minPriorityLevel,
+ minExpirationTime,
deadline,
);
error = null;
@@ -1076,19 +1106,21 @@ module.exports = function(
break;
}
- // Reset the priority context to its previous value.
- priorityContext = previousPriorityContext;
-
// If we're inside a callback, set this to false, since we just flushed it.
if (deadline !== null) {
isCallbackScheduled = false;
}
// If there's remaining async work, make sure we schedule another callback.
- if (nextPriorityLevel > TaskPriority && !isCallbackScheduled) {
+ if (
+ nextRenderExpirationTime > mostRecentCurrentTime &&
+ !isCallbackScheduled
+ ) {
scheduleDeferredCallback(performDeferredWork);
isCallbackScheduled = true;
}
+ priorityContext = previousPriorityContext;
+
const errorToThrow = firstUncaughtError;
// We're done performing work. Time to clean up.
@@ -1353,8 +1385,8 @@ module.exports = function(
}
}
- function scheduleRoot(root: FiberRoot, priorityLevel: PriorityLevel) {
- if (priorityLevel === NoWork) {
+ function scheduleRoot(root: FiberRoot, expirationTime: ExpirationTime) {
+ if (expirationTime === NoWork) {
return;
}
@@ -1372,13 +1404,13 @@ module.exports = function(
}
}
- function scheduleUpdate(fiber: Fiber, priorityLevel: PriorityLevel) {
- return scheduleUpdateImpl(fiber, priorityLevel, false);
+ function scheduleUpdate(fiber: Fiber, expirationTime: ExpirationTime) {
+ return scheduleUpdateImpl(fiber, expirationTime, false);
}
function scheduleUpdateImpl(
fiber: Fiber,
- priorityLevel: PriorityLevel,
+ expirationTime: ExpirationTime,
isErrorRecovery: boolean,
) {
if (__DEV__) {
@@ -1396,7 +1428,7 @@ module.exports = function(
);
}
- if (!isPerformingWork && priorityLevel <= nextPriorityLevel) {
+ if (!isPerformingWork && expirationTime <= nextRenderExpirationTime) {
// We must reset the current unit of work pointer so that we restart the
// search from the root during the next tick, in case there is now higher
// priority work somewhere earlier than before.
@@ -1413,36 +1445,40 @@ module.exports = function(
let node = fiber;
let shouldContinue = true;
while (node !== null && shouldContinue) {
- // Walk the parent path to the root and update each node's priority. Once
- // we reach a node whose priority matches (and whose alternate's priority
- // matches) we can exit safely knowing that the rest of the path is correct.
+ // Walk the parent path to the root and update each node's expiration
+ // time. Once we reach a node whose expiration matches (and whose
+ // alternate's expiration matches) we can exit safely knowing that the
+ // rest of the path is correct.
shouldContinue = false;
if (
- node.pendingWorkPriority === NoWork ||
- node.pendingWorkPriority > priorityLevel
+ node.expirationTime === NoWork ||
+ node.expirationTime > expirationTime
) {
- // Priority did not match. Update and keep going.
+ // Expiration time did not match. Update and keep going.
shouldContinue = true;
- node.pendingWorkPriority = priorityLevel;
+ node.expirationTime = expirationTime;
}
if (node.alternate !== null) {
if (
- node.alternate.pendingWorkPriority === NoWork ||
- node.alternate.pendingWorkPriority > priorityLevel
+ node.alternate.expirationTime === NoWork ||
+ node.alternate.expirationTime > expirationTime
) {
- // Priority did not match. Update and keep going.
+ // Expiration time did not match. Update and keep going.
shouldContinue = true;
- node.alternate.pendingWorkPriority = priorityLevel;
+ node.alternate.expirationTime = expirationTime;
}
}
if (node.return === null) {
if (node.tag === HostRoot) {
const root: FiberRoot = (node.stateNode: any);
- scheduleRoot(root, priorityLevel);
+ scheduleRoot(root, expirationTime);
if (!isPerformingWork) {
+ const priorityLevel = expirationTimeToPriorityLevel(
+ mostRecentCurrentTime,
+ expirationTime,
+ );
switch (priorityLevel) {
case SynchronousPriority:
- // Perform this update now.
if (isUnbatchingUpdates) {
// We're inside unbatchedUpdates, which is inside either
// batchedUpdates or a lifecycle. We should only flush
@@ -1457,11 +1493,12 @@ module.exports = function(
invariant(
isBatchingUpdates,
'Task updates can only be scheduled as a nested update or ' +
- 'inside batchedUpdates.',
+ 'inside batchedUpdates. This error is likely caused by a ' +
+ 'bug in React. Please file an issue.',
);
break;
default:
- // Schedule a callback to perform the work later.
+ // This update is async. Schedule a callback.
if (!isCallbackScheduled) {
scheduleDeferredCallback(performDeferredWork);
isCallbackScheduled = true;
@@ -1484,7 +1521,13 @@ module.exports = function(
function getPriorityContext(
fiber: Fiber,
forceAsync: boolean,
- ): PriorityLevel {
+ ): PriorityLevel | null {
+ if (isPerformingWork && !isCommitting) {
+ // Updates during the render phase should expire at the same time as
+ // the work that is being rendered. Return null to indicate.
+ return null;
+ }
+
let priorityLevel = priorityContext;
if (priorityLevel === NoWork) {
if (
@@ -1509,8 +1552,31 @@ module.exports = function(
return priorityLevel;
}
+ function getExpirationTimeForPriority(
+ currentTime: ExpirationTime,
+ priorityLevel: PriorityLevel | null,
+ ): ExpirationTime {
+ if (priorityLevel === null) {
+ // A priorityLevel of null indicates that this update should expire at
+ // the same time as whatever is currently being rendered.
+ return nextRenderExpirationTime;
+ }
+ return priorityToExpirationTime(currentTime, priorityLevel);
+ }
+
function scheduleErrorRecovery(fiber: Fiber) {
- scheduleUpdateImpl(fiber, TaskPriority, true);
+ const taskTime = getExpirationTimeForPriority(
+ mostRecentCurrentTime,
+ TaskPriority,
+ );
+ scheduleUpdateImpl(fiber, taskTime, true);
+ }
+
+ function recalculateCurrentTime(): ExpirationTime {
+ // Subtract initial time so it fits inside 32bits
+ const ms = now() - startTime;
+ mostRecentCurrentTime = msToExpirationTime(ms);
+ return mostRecentCurrentTime;
}
function batchedUpdates(fn: (a: A) => R, a: A): R {
@@ -1575,6 +1641,8 @@ module.exports = function(
return {
scheduleUpdate: scheduleUpdate,
getPriorityContext: getPriorityContext,
+ recalculateCurrentTime: recalculateCurrentTime,
+ getExpirationTimeForPriority: getExpirationTimeForPriority,
batchedUpdates: batchedUpdates,
unbatchedUpdates: unbatchedUpdates,
flushSync: flushSync,
diff --git a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js
index 39f483b7d78d..596ed9d41c98 100644
--- a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js
+++ b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js
@@ -12,14 +12,11 @@
import type {Fiber} from 'ReactFiber';
import type {PriorityLevel} from 'ReactPriorityLevel';
+import type {ExpirationTime} from 'ReactFiberExpirationTime';
const {Callback: CallbackEffect} = require('ReactTypeOfSideEffect');
-const {
- NoWork,
- SynchronousPriority,
- TaskPriority,
-} = require('ReactPriorityLevel');
+const {NoWork} = require('ReactFiberExpirationTime');
const {ClassComponent, HostRoot} = require('ReactTypeOfWork');
@@ -35,8 +32,9 @@ type PartialState =
// Callbacks are not validated until invocation
type Callback = mixed;
-type Update = {
- priorityLevel: PriorityLevel,
+export type Update = {
+ priorityLevel: PriorityLevel | null,
+ expirationTime: ExpirationTime,
partialState: PartialState,
callback: Callback | null,
isReplace: boolean,
@@ -69,25 +67,6 @@ export type UpdateQueue = {
let _queue1;
let _queue2;
-function comparePriority(a: PriorityLevel, b: PriorityLevel): number {
- // When comparing update priorities, treat sync and Task work as equal.
- // TODO: Could we avoid the need for this by always coercing sync priority
- // to Task when scheduling an update?
- if (
- (a === TaskPriority || a === SynchronousPriority) &&
- (b === TaskPriority || b === SynchronousPriority)
- ) {
- return 0;
- }
- if (a === NoWork && b !== NoWork) {
- return -255;
- }
- if (a !== NoWork && b === NoWork) {
- return 255;
- }
- return a - b;
-}
-
function createUpdateQueue(): UpdateQueue {
const queue: UpdateQueue = {
first: null,
@@ -104,6 +83,7 @@ function createUpdateQueue(): UpdateQueue {
function cloneUpdate(update: Update): Update {
return {
priorityLevel: update.priorityLevel,
+ expirationTime: update.expirationTime,
partialState: update.partialState,
callback: update.callback,
isReplace: update.isReplace,
@@ -113,14 +93,31 @@ function cloneUpdate(update: Update): Update {
};
}
+const COALESCENCE_THRESHOLD: ExpirationTime = 10;
+
function insertUpdateIntoQueue(
queue: UpdateQueue,
update: Update,
insertAfter: Update | null,
insertBefore: Update | null,
+ currentTime: ExpirationTime,
) {
if (insertAfter !== null) {
insertAfter.next = update;
+ // If we receive multiple updates to the same fiber at the same priority
+ // level, we coalesce them by assigning the same expiration time, so that
+ // they all flush at the same time. Because this causes an interruption, it
+ // could lead to starvation, so we stop coalescing once the time until the
+ // expiration time reaches a certain threshold.
+ if (
+ insertAfter !== null &&
+ insertAfter.priorityLevel === update.priorityLevel
+ ) {
+ const coalescedTime = insertAfter.expirationTime;
+ if (coalescedTime - currentTime > COALESCENCE_THRESHOLD) {
+ update.expirationTime = coalescedTime;
+ }
+ }
} else {
// This is the first item in the queue.
update.next = queue.first;
@@ -138,13 +135,10 @@ function insertUpdateIntoQueue(
// Returns the update after which the incoming update should be inserted into
// the queue, or null if it should be inserted at beginning.
function findInsertionPosition(queue, update): Update | null {
- const priorityLevel = update.priorityLevel;
+ const expirationTime = update.expirationTime;
let insertAfter = null;
let insertBefore = null;
- if (
- queue.last !== null &&
- comparePriority(queue.last.priorityLevel, priorityLevel) <= 0
- ) {
+ if (queue.last !== null && queue.last.expirationTime <= expirationTime) {
// Fast path for the common case where the update should be inserted at
// the end of the queue.
insertAfter = queue.last;
@@ -152,7 +146,7 @@ function findInsertionPosition(queue, update): Update | null {
insertBefore = queue.first;
while (
insertBefore !== null &&
- comparePriority(insertBefore.priorityLevel, priorityLevel) <= 0
+ insertBefore.expirationTime <= expirationTime
) {
insertAfter = insertBefore;
insertBefore = insertBefore.next;
@@ -213,7 +207,11 @@ function ensureUpdateQueues(fiber: Fiber) {
// we shouldn't make a copy.
//
// If the update is cloned, it returns the cloned update.
-function insertUpdate(fiber: Fiber, update: Update): Update | null {
+function insertUpdate(
+ fiber: Fiber,
+ update: Update,
+ currentTime: ExpirationTime,
+): Update | null {
// We'll have at least one and at most two distinct update queues.
ensureUpdateQueues(fiber);
const queue1 = _queue1;
@@ -240,7 +238,13 @@ function insertUpdate(fiber: Fiber, update: Update): Update | null {
if (queue2 === null) {
// If there's no alternate queue, there's nothing else to do but insert.
- insertUpdateIntoQueue(queue1, update, insertAfter1, insertBefore1);
+ insertUpdateIntoQueue(
+ queue1,
+ update,
+ insertAfter1,
+ insertBefore1,
+ currentTime,
+ );
return null;
}
@@ -252,7 +256,13 @@ function insertUpdate(fiber: Fiber, update: Update): Update | null {
// Now we can insert into the first queue. This must come after finding both
// insertion positions because it mutates the list.
- insertUpdateIntoQueue(queue1, update, insertAfter1, insertBefore1);
+ insertUpdateIntoQueue(
+ queue1,
+ update,
+ insertAfter1,
+ insertBefore1,
+ currentTime,
+ );
// See if the insertion positions are equal. Be careful to only compare
// non-null values.
@@ -275,7 +285,13 @@ function insertUpdate(fiber: Fiber, update: Update): Update | null {
// The insertion positions are different, so we need to clone the update and
// insert the clone into the alternate queue.
const update2 = cloneUpdate(update);
- insertUpdateIntoQueue(queue2, update2, insertAfter2, insertBefore2);
+ insertUpdateIntoQueue(
+ queue2,
+ update2,
+ insertAfter2,
+ insertBefore2,
+ currentTime,
+ );
return update2;
}
}
@@ -284,10 +300,13 @@ function addUpdate(
fiber: Fiber,
partialState: PartialState | null,
callback: mixed,
- priorityLevel: PriorityLevel,
+ priorityLevel: PriorityLevel | null,
+ expirationTime: ExpirationTime,
+ currentTime: ExpirationTime,
): void {
const update = {
priorityLevel,
+ expirationTime,
partialState,
callback,
isReplace: false,
@@ -295,7 +314,7 @@ function addUpdate(
isTopLevelUnmount: false,
next: null,
};
- insertUpdate(fiber, update);
+ insertUpdate(fiber, update, currentTime);
}
exports.addUpdate = addUpdate;
@@ -303,10 +322,13 @@ function addReplaceUpdate(
fiber: Fiber,
state: any | null,
callback: Callback | null,
- priorityLevel: PriorityLevel,
+ priorityLevel: PriorityLevel | null,
+ expirationTime: ExpirationTime,
+ currentTime: ExpirationTime,
): void {
const update = {
priorityLevel,
+ expirationTime,
partialState: state,
callback,
isReplace: true,
@@ -314,17 +336,20 @@ function addReplaceUpdate(
isTopLevelUnmount: false,
next: null,
};
- insertUpdate(fiber, update);
+ insertUpdate(fiber, update, currentTime);
}
exports.addReplaceUpdate = addReplaceUpdate;
function addForceUpdate(
fiber: Fiber,
callback: Callback | null,
- priorityLevel: PriorityLevel,
+ priorityLevel: PriorityLevel | null,
+ expirationTime: ExpirationTime,
+ currentTime: ExpirationTime,
): void {
const update = {
priorityLevel,
+ expirationTime,
partialState: null,
callback,
isReplace: false,
@@ -332,11 +357,11 @@ function addForceUpdate(
isTopLevelUnmount: false,
next: null,
};
- insertUpdate(fiber, update);
+ insertUpdate(fiber, update, currentTime);
}
exports.addForceUpdate = addForceUpdate;
-function getUpdatePriority(fiber: Fiber): PriorityLevel {
+function getUpdateExpirationTime(fiber: Fiber): ExpirationTime {
const updateQueue = fiber.updateQueue;
if (updateQueue === null) {
return NoWork;
@@ -344,20 +369,23 @@ function getUpdatePriority(fiber: Fiber): PriorityLevel {
if (fiber.tag !== ClassComponent && fiber.tag !== HostRoot) {
return NoWork;
}
- return updateQueue.first !== null ? updateQueue.first.priorityLevel : NoWork;
+ return updateQueue.first !== null ? updateQueue.first.expirationTime : NoWork;
}
-exports.getUpdatePriority = getUpdatePriority;
+exports.getUpdateExpirationTime = getUpdateExpirationTime;
function addTopLevelUpdate(
fiber: Fiber,
partialState: PartialState,
callback: Callback | null,
- priorityLevel: PriorityLevel,
+ priorityLevel: PriorityLevel | null,
+ expirationTime: ExpirationTime,
+ currentTime: ExpirationTime,
): void {
const isTopLevelUnmount = partialState.element === null;
const update = {
priorityLevel,
+ expirationTime,
partialState,
callback,
isReplace: false,
@@ -365,7 +393,7 @@ function addTopLevelUpdate(
isTopLevelUnmount,
next: null,
};
- const update2 = insertUpdate(fiber, update);
+ const update2 = insertUpdate(fiber, update, currentTime);
if (isTopLevelUnmount) {
// TODO: Redesign the top-level mount/update/unmount API to avoid this
@@ -404,7 +432,7 @@ function beginUpdateQueue(
instance: any,
prevState: any,
props: any,
- priorityLevel: PriorityLevel,
+ renderExpirationTime: ExpirationTime,
): any {
if (current !== null && current.updateQueue === queue) {
// We need to create a work-in-progress queue, by cloning the current queue.
@@ -434,10 +462,7 @@ function beginUpdateQueue(
let state = prevState;
let dontMutatePrevState = true;
let update = queue.first;
- while (
- update !== null &&
- comparePriority(update.priorityLevel, priorityLevel) <= 0
- ) {
+ while (update !== null && update.expirationTime <= renderExpirationTime) {
// Remove each update from the queue right before it is processed. That way
// if setState is called from inside an updater function, the new update
// will be inserted in the correct position.
diff --git a/src/renderers/shared/fiber/__tests__/ReactExpiration-test.js b/src/renderers/shared/fiber/__tests__/ReactExpiration-test.js
new file mode 100644
index 000000000000..7b9121f19a20
--- /dev/null
+++ b/src/renderers/shared/fiber/__tests__/ReactExpiration-test.js
@@ -0,0 +1,125 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+'use strict';
+
+var React;
+var ReactNoop;
+
+describe('ReactExpiration', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ React = require('react');
+ ReactNoop = require('react-noop-renderer');
+ });
+
+ function span(prop) {
+ return {type: 'span', children: [], prop};
+ }
+
+ it('increases priority of updates as time progresses', () => {
+ ReactNoop.render();
+
+ expect(ReactNoop.getChildren()).toEqual([]);
+
+ // Nothing has expired yet because time hasn't advanced.
+ ReactNoop.flushExpired();
+ expect(ReactNoop.getChildren()).toEqual([]);
+
+ // Advance by 300ms, not enough to expire the low pri update.
+ ReactNoop.expire(300);
+ ReactNoop.flushExpired();
+ expect(ReactNoop.getChildren()).toEqual([]);
+
+ // Advance by another second. Now the update should expire and flush.
+ ReactNoop.expire(1000);
+ ReactNoop.flushExpired();
+ expect(ReactNoop.getChildren()).toEqual([span('done')]);
+ });
+
+ it('coalesces updates to the same component', () => {
+ const foos = [];
+ class Foo extends React.Component {
+ constructor() {
+ super();
+ this.state = {step: 0};
+ foos.push(this);
+ }
+ render() {
+ return ;
+ }
+ }
+
+ ReactNoop.render([, ]);
+ ReactNoop.flush();
+ const [a, b] = foos;
+
+ a.setState({step: 1});
+
+ // Advance time by 500ms.
+ ReactNoop.expire(500);
+
+ // Update A again. This update should coalesce with the previous update.
+ a.setState({step: 2});
+ // Update B. This is the first update, so it has nothing to coalesce with.
+ b.setState({step: 1});
+
+ // Advance time. This should be enough to flush both updates to A, but not
+ // the update to B. If only the first update to A flushes, but not the
+ // second, then it wasn't coalesced properly.
+ ReactNoop.expire(600);
+ ReactNoop.flushExpired();
+ expect(ReactNoop.getChildren()).toEqual([span(2), span(0)]);
+
+ // Now expire the update to B.
+ ReactNoop.expire(500);
+ ReactNoop.flushExpired();
+ expect(ReactNoop.getChildren()).toEqual([span(2), span(1)]);
+ });
+
+ it('stops coalescing after a certain threshold', () => {
+ let instance;
+ class Foo extends React.Component {
+ state = {step: 0};
+ render() {
+ instance = this;
+ return ;
+ }
+ }
+
+ ReactNoop.render();
+ ReactNoop.flush();
+
+ instance.setState({step: 1});
+
+ // Advance time by 500 ms.
+ ReactNoop.expire(500);
+
+ // Update again. This update should coalesce with the previous update.
+ instance.setState({step: 2});
+
+ // Advance time by 480ms. Not enough to expire the updates.
+ ReactNoop.expire(480);
+ ReactNoop.flushExpired();
+ expect(ReactNoop.getChildren()).toEqual([span(0)]);
+
+ // Update again. This update should NOT be coalesced, because the
+ // previous updates have almost expired.
+ instance.setState({step: 3});
+
+ // Advance time. This should expire the first two updates,
+ // but not the third.
+ ReactNoop.expire(500);
+ ReactNoop.flushExpired();
+ expect(ReactNoop.getChildren()).toEqual([span(2)]);
+
+ // Now expire the remaining update.
+ ReactNoop.expire(1000);
+ ReactNoop.flushExpired();
+ expect(ReactNoop.getChildren()).toEqual([span(3)]);
+ });
+});
diff --git a/src/renderers/shared/fiber/__tests__/ReactFiberHostContext-test.js b/src/renderers/shared/fiber/__tests__/ReactFiberHostContext-test.js
index 7c00d20f50f7..60416632715f 100644
--- a/src/renderers/shared/fiber/__tests__/ReactFiberHostContext-test.js
+++ b/src/renderers/shared/fiber/__tests__/ReactFiberHostContext-test.js
@@ -46,6 +46,9 @@ describe('ReactFiberHostContext', () => {
appendChildToContainer: function() {
return null;
},
+ now: function() {
+ return 0;
+ },
useSyncScheduling: true,
});
diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js
index fdeefaa3d975..d5cdd22ae431 100644
--- a/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js
+++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js
@@ -54,6 +54,14 @@ describe('ReactIncrementalTriangle', () => {
};
}
+ const EXPIRE = 'EXPIRE';
+ function expire(ms) {
+ return {
+ type: EXPIRE,
+ ms,
+ };
+ }
+
function TriangleSimulator() {
let triangles = [];
let leafTriangles = [];
@@ -212,6 +220,9 @@ describe('ReactIncrementalTriangle', () => {
targetTriangle.activate();
}
break;
+ case EXPIRE:
+ ReactNoop.expire(action.ms);
+ break;
default:
break;
}
@@ -251,7 +262,7 @@ describe('ReactIncrementalTriangle', () => {
}
function randomAction() {
- switch (randomInteger(0, 4)) {
+ switch (randomInteger(0, 5)) {
case 0:
return flush(randomInteger(0, totalTriangles * 1.5));
case 1:
@@ -260,6 +271,8 @@ describe('ReactIncrementalTriangle', () => {
return interrupt();
case 3:
return toggle(randomInteger(0, totalChildren));
+ case 4:
+ return expire(randomInteger(0, 1500));
default:
throw new Error('Switch statement should be exhaustive');
}
@@ -290,6 +303,9 @@ describe('ReactIncrementalTriangle', () => {
case TOGGLE:
result += `toggle(${action.childIndex})`;
break;
+ case EXPIRE:
+ result += `expire(${action.ms})`;
+ break;
default:
throw new Error('Switch statement should be exhaustive');
}
diff --git a/src/renderers/testing/ReactTestRendererFiberEntry.js b/src/renderers/testing/ReactTestRendererFiberEntry.js
index 6779c0dc3662..2a6d9e1ea576 100644
--- a/src/renderers/testing/ReactTestRendererFiberEntry.js
+++ b/src/renderers/testing/ReactTestRendererFiberEntry.js
@@ -247,6 +247,11 @@ var TestRenderer = ReactFiberReconciler({
useSyncScheduling: true,
getPublicInstance,
+
+ now(): number {
+ // Test renderer does not use expiration
+ return 0;
+ },
});
var defaultTestOptions = {