diff --git a/fixtures/fiber-triangle/index.html b/fixtures/fiber-triangle/index.html
index d2541eee1e93..0458a4c94fbc 100644
--- a/fixtures/fiber-triangle/index.html
+++ b/fixtures/fiber-triangle/index.html
@@ -26,6 +26,7 @@
this.enter()} onMouseLeave={() => this.leave()}>
- {this.state.hover ? '*' + props.text + '*' : props.text}
+
this.div = div} style={style} onMouseEnter={() => this.enter()} onMouseLeave={() => this.leave()}>
+ {this.state.hover ? `*${props.text}*` : props.text}
);
}
diff --git a/scripts/fiber/tests-failing.txt b/scripts/fiber/tests-failing.txt
index 789af77694f0..52415452247c 100644
--- a/scripts/fiber/tests-failing.txt
+++ b/scripts/fiber/tests-failing.txt
@@ -3,3 +3,6 @@ src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
* should not blow away user-entered text on successful reconnect to a controlled checkbox
* should not blow away user-selected value on successful reconnect to an uncontrolled select
* should not blow away user-selected value on successful reconnect to an controlled select
+
+src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js
+* resumes work by comparing the priority at which the work-in-progress was created/updated
diff --git a/scripts/fiber/tests-passing-except-dev.txt b/scripts/fiber/tests-passing-except-dev.txt
index 4ae14d31d6c9..c91cd76d319c 100644
--- a/scripts/fiber/tests-passing-except-dev.txt
+++ b/scripts/fiber/tests-passing-except-dev.txt
@@ -86,12 +86,6 @@ src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
* renders a div with a single child surrounded by whitespace with client render on top of bad server markup
* renders >,<, and & as single child with client render on top of bad server markup
* renders >,<, and & as multiple children with client render on top of bad server markup
-* throws when rendering a string component with clean client render
-* throws when rendering a string component with client render on top of bad server markup
-* throws when rendering an undefined component with clean client render
-* throws when rendering an undefined component with client render on top of bad server markup
-* throws when rendering a number component with clean client render
-* throws when rendering a number component with client render on top of bad server markup
* renders an input with a value and an onChange with client render on top of bad server markup
* renders an input with a value and readOnly with client render on top of bad server markup
* renders an input with a value and no onChange/readOnly with client render on top of bad server markup
diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt
index 930fb89124d8..f07e7eb25840 100644
--- a/scripts/fiber/tests-passing.txt
+++ b/scripts/fiber/tests-passing.txt
@@ -1121,8 +1121,14 @@ src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
* renders >,<, and & as multiple children with clean client render
* renders >,<, and & as multiple children with client render on top of good server markup
* throws when rendering a string component with server string render
+* throws when rendering a string component with clean client render
+* throws when rendering a string component with client render on top of bad server markup
* throws when rendering an undefined component with server string render
+* throws when rendering an undefined component with clean client render
+* throws when rendering an undefined component with client render on top of bad server markup
* throws when rendering a number component with server string render
+* throws when rendering a number component with clean client render
+* throws when rendering a number component with client render on top of bad server markup
* throws when rendering null with server string render
* throws when rendering null with clean client render
* throws when rendering null with client render on top of bad server markup
@@ -1860,6 +1866,8 @@ src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js
* can defer side-effects and resume them later on
* can defer side-effects and reuse them later - complex
* deprioritizes setStates that happens within a deprioritized tree
+* does not drop priority from a progressed subtree
+* does not complete already completed work
* calls callback after update is flushed
* calls setState callback even if component bails out
* calls componentWillUnmount after a deletion, even if nested
@@ -1867,6 +1875,9 @@ src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js
* invokes ref callbacks after insertion/update/unmount
* supports string refs
+src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js
+* works
+
src/renderers/shared/fiber/__tests__/ReactIncrementalUpdates-test.js
* applies updates in order of priority
* applies updates with equal priority in insertion order
diff --git a/src/renderers/__tests__/ReactComponent-test.js b/src/renderers/__tests__/ReactComponent-test.js
index 1ea4d6dddce5..9f330f7b0827 100644
--- a/src/renderers/__tests__/ReactComponent-test.js
+++ b/src/renderers/__tests__/ReactComponent-test.js
@@ -44,7 +44,12 @@ describe('ReactComponent', () => {
var instance =
;
expect(function() {
instance = ReactTestUtils.renderIntoDocument(instance);
- }).toThrow();
+ }).toThrow(
+ 'Only a ReactOwner can have refs. You might be adding a ref to a ' +
+ "component that was not created inside a component's `render` " +
+ 'method, or you have multiple copies of React loaded (details: ' +
+ 'https://fb.me/react-refs-must-have-owner).',
+ );
});
it('should warn when children are mutated during render', () => {
diff --git a/src/renderers/dom/fiber/ReactDOMFiber.js b/src/renderers/dom/fiber/ReactDOMFiber.js
index b17a2d40f6d2..95f8be45e3e8 100644
--- a/src/renderers/dom/fiber/ReactDOMFiber.js
+++ b/src/renderers/dom/fiber/ReactDOMFiber.js
@@ -115,7 +115,9 @@ function getReactRootElementInContainer(container: any) {
function shouldReuseContent(container) {
const rootElement = getReactRootElementInContainer(container);
- return !!(rootElement && rootElement.getAttribute(ID_ATTRIBUTE_NAME));
+ return !!(rootElement &&
+ rootElement.nodeType === ELEMENT_NODE &&
+ rootElement.getAttribute(ID_ATTRIBUTE_NAME));
}
function shouldAutoFocusHostComponent(type: string, props: Props): boolean {
diff --git a/src/renderers/noop/ReactNoop.js b/src/renderers/noop/ReactNoop.js
index 815b3d9748e8..b15127494ae9 100644
--- a/src/renderers/noop/ReactNoop.js
+++ b/src/renderers/noop/ReactNoop.js
@@ -212,6 +212,40 @@ var rootContainers = new Map();
var roots = new Map();
var DEFAULT_ROOT_ID = '
';
+let yieldBeforeNextUnitOfWork = false;
+let yieldValue = null;
+
+function* flushUnitsOfWork(n: number): Generator {
+ var didStop = false;
+ while (!didStop && scheduledDeferredCallback !== null) {
+ var cb = scheduledDeferredCallback;
+ scheduledDeferredCallback = null;
+ yieldBeforeNextUnitOfWork = false;
+ yieldValue = null;
+ var unitsRemaining = n;
+ var didYield = false;
+ cb({
+ timeRemaining() {
+ if (yieldBeforeNextUnitOfWork) {
+ didYield = true;
+ return 0;
+ }
+ if (unitsRemaining-- > 0) {
+ return 999;
+ }
+ didStop = true;
+ return 0;
+ },
+ });
+
+ if (didYield) {
+ const valueToYield = yieldValue;
+ yieldValue = null;
+ yield valueToYield;
+ }
+ }
+}
+
var ReactNoop = {
getChildren(rootID: string = DEFAULT_ROOT_ID) {
const container = rootContainers.get(rootID);
@@ -277,22 +311,17 @@ var ReactNoop = {
},
flushDeferredPri(timeout: number = Infinity) {
- var cb = scheduledDeferredCallback;
- if (cb === null) {
- return;
+ // The legacy version of this function decremented the timeout before
+ // returning the new time.
+ // TODO: Convert tests to use flushUnitsOfWork or flushAndYield instead.
+ const n = timeout / 5 - 1;
+ const iterator = flushUnitsOfWork(n);
+ let value = iterator.next();
+ while (!value.done) {
+ value = iterator.next();
}
- scheduledDeferredCallback = null;
- var timeRemaining = timeout;
- cb({
- timeRemaining() {
- // Simulate a fix amount of time progressing between each call.
- timeRemaining -= 5;
- if (timeRemaining < 0) {
- timeRemaining = 0;
- }
- return timeRemaining;
- },
- });
+ // Don't flush animation priority in this legacy function. Some tests may
+ // still rely on this behavior.
},
flush() {
@@ -300,6 +329,30 @@ var ReactNoop = {
ReactNoop.flushDeferredPri();
},
+ *flushAndYield(unitsOfWork: number = Infinity): Generator {
+ for (const value of flushUnitsOfWork(unitsOfWork)) {
+ yield value;
+ }
+ ReactNoop.flushAnimationPri();
+ },
+
+ flushUnitsOfWork(n: number) {
+ const iterator = flushUnitsOfWork(n);
+ let value = iterator.next();
+ while (!value.done) {
+ value = iterator.next();
+ }
+ // TODO: We should always flush animation priority after flushing normal/low
+ // priority. Move this to flushUnitsOfWork generator once tests
+ // are converted.
+ ReactNoop.flushAnimationPri();
+ },
+
+ yield(value: mixed) {
+ yieldBeforeNextUnitOfWork = true;
+ yieldValue = value;
+ },
+
performAnimationWork(fn: Function) {
NoopRenderer.performWithPriority(AnimationPriority, fn);
},
@@ -377,7 +430,7 @@ var ReactNoop = {
if (fiber.updateQueue) {
logUpdateQueue(fiber.updateQueue, depth);
}
- const childInProgress = fiber.progressedChild;
+ const childInProgress = fiber.progressedWork.child;
if (childInProgress && childInProgress !== fiber.child) {
log(
' '.repeat(depth + 1) + 'IN PROGRESS: ' + fiber.progressedPriority,
diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js
index 3587750b2384..b6d616e09e56 100644
--- a/src/renderers/shared/fiber/ReactChildFiber.js
+++ b/src/renderers/shared/fiber/ReactChildFiber.js
@@ -16,7 +16,6 @@ import type {ReactElement} from 'ReactElementType';
import type {ReactCoroutine, ReactPortal, ReactYield} from 'ReactTypes';
import type {Fiber} from 'ReactFiber';
import type {ReactInstance} from 'ReactInstanceType';
-import type {PriorityLevel} from 'ReactPriorityLevel';
var REACT_ELEMENT_TYPE = require('ReactElementSymbol');
var {REACT_COROUTINE_TYPE, REACT_YIELD_TYPE} = require('ReactCoroutine');
@@ -26,9 +25,9 @@ var ReactFiber = require('ReactFiber');
var ReactTypeOfSideEffect = require('ReactTypeOfSideEffect');
var ReactTypeOfWork = require('ReactTypeOfWork');
-var emptyObject = require('fbjs/lib/emptyObject');
var getIteratorFn = require('getIteratorFn');
var invariant = require('fbjs/lib/invariant');
+var emptyObject = require('fbjs/lib/emptyObject');
var ReactFeatureFlags = require('ReactFeatureFlags');
if (__DEV__) {
@@ -76,7 +75,7 @@ if (__DEV__) {
}
const {
- cloneFiber,
+ createWorkInProgress,
createFiberFromElement,
createFiberFromFragment,
createFiberFromText,
@@ -100,50 +99,61 @@ const {
const {NoEffect, Placement, Deletion} = ReactTypeOfSideEffect;
function coerceRef(current: Fiber | null, element: ReactElement) {
- let mixedRef = element.ref;
+ const mixedRef = element.ref;
if (mixedRef !== null && typeof mixedRef !== 'function') {
- if (element._owner) {
- const owner: ?(Fiber | ReactInstance) = (element._owner: any);
- let inst;
- if (owner) {
- if (typeof owner.tag === 'number') {
- const ownerFiber = ((owner: any): Fiber);
- invariant(
- ownerFiber.tag === ClassComponent,
- 'Stateless function components cannot have refs.',
- );
- inst = ownerFiber.stateNode;
- } else {
- // Stack
- inst = (owner: any).getPublicInstance();
- }
- }
- invariant(
- inst,
- 'Missing owner for string ref %s. This error is likely caused by a ' +
- 'bug in React. Please file an issue.',
- mixedRef,
- );
- const stringRef = '' + mixedRef;
- // Check if previous string ref matches new string ref
- if (
- current !== null &&
- current.ref !== null &&
- current.ref._stringRef === stringRef
- ) {
- return current.ref;
+ const owner: ?(Fiber | ReactInstance) = (element._owner: any);
+ invariant(
+ owner != null,
+ 'Only a ReactOwner can have refs. You might be adding a ref to a ' +
+ "component that was not created inside a component's `render` " +
+ 'method, or you have multiple copies of React loaded (details: ' +
+ 'https://fb.me/react-refs-must-have-owner).',
+ );
+ let inst;
+ if (owner) {
+ if (typeof owner.tag === 'number') {
+ const ownerFiber = ((owner: any): Fiber);
+ invariant(
+ ownerFiber.tag === ClassComponent,
+ 'Stateless function components cannot have refs.',
+ );
+ inst = ownerFiber.stateNode;
+ } else {
+ // Stack
+ inst = (owner: any).getPublicInstance();
}
- const ref = function(value) {
- const refs = inst.refs === emptyObject ? (inst.refs = {}) : inst.refs;
- if (value === null) {
- delete refs[stringRef];
- } else {
- refs[stringRef] = value;
- }
- };
- ref._stringRef = stringRef;
- return ref;
}
+ invariant(
+ inst,
+ 'Missing owner for string ref %s. This error is likely caused by a ' +
+ 'bug in React. Please file an issue.',
+ mixedRef,
+ );
+ const stringRef = '' + mixedRef;
+ // Check if previous string ref matches new string ref
+ if (
+ current !== null &&
+ current.ref !== null &&
+ current.ref._stringRef === stringRef
+ ) {
+ return current.ref;
+ }
+ const ref = function(value) {
+ const refs =
+ // TODO: Comparison to emptyObject always fails. Don't know why.
+ // inst.refs !== emptyObject ? inst.refs : (inst.refs = {});
+ typeof Object.isExtensible === 'function' &&
+ Object.isExtensible(inst.refs)
+ ? inst.refs
+ : (inst.refs = {});
+ if (value === null) {
+ delete refs[stringRef];
+ } else {
+ refs[stringRef] = value;
+ }
+ };
+ ref._stringRef = stringRef;
+ return ref;
}
return mixedRef;
}
@@ -178,6 +188,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
// Noop.
return;
}
+
if (!shouldClone) {
// When we're reconciling in place we have a work in progress copy. We
// actually want the current copy. If there is no current copy, then we
@@ -187,13 +198,14 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
}
childToDelete = childToDelete.alternate;
}
+
// Deletions are added in reversed order so we add it to the front.
- const last = returnFiber.progressedLastDeletion;
+ const last = returnFiber.lastDeletion;
if (last !== null) {
last.nextEffect = childToDelete;
- returnFiber.progressedLastDeletion = childToDelete;
+ returnFiber.lastDeletion = childToDelete;
} else {
- returnFiber.progressedFirstDeletion = returnFiber.progressedLastDeletion = childToDelete;
+ returnFiber.firstDeletion = returnFiber.lastDeletion = childToDelete;
}
childToDelete.nextEffect = null;
childToDelete.effectTag = Deletion;
@@ -239,19 +251,16 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
return existingChildren;
}
- function useFiber(fiber: Fiber, priority: PriorityLevel): Fiber {
+ function useFiber(fiber: Fiber, pendingProps: mixed): 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 = cloneFiber(fiber, priority);
+ const clone = createWorkInProgress(fiber, pendingProps);
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
- // down-prioritized.
- fiber.pendingWorkPriority = priority;
+ fiber.pendingProps = pendingProps;
fiber.effectTag = NoEffect;
fiber.index = 0;
fiber.sibling = null;
@@ -300,21 +309,18 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
current: Fiber | null,
textContent: string,
- priority: PriorityLevel,
) {
if (current === null || current.tag !== HostText) {
// Insert
const created = createFiberFromText(
textContent,
returnFiber.internalContextTag,
- priority,
);
created.return = returnFiber;
return created;
} else {
// Update
- const existing = useFiber(current, priority);
- existing.pendingProps = textContent;
+ const existing = useFiber(current, textContent);
existing.return = returnFiber;
return existing;
}
@@ -324,23 +330,20 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
current: Fiber | null,
element: ReactElement,
- priority: PriorityLevel,
): Fiber {
if (current === null || current.type !== element.type) {
// Insert
const created = createFiberFromElement(
element,
returnFiber.internalContextTag,
- priority,
);
created.ref = coerceRef(current, element);
created.return = returnFiber;
return created;
} else {
// Move based on index
- const existing = useFiber(current, priority);
+ const existing = useFiber(current, element.props);
existing.ref = coerceRef(current, element);
- existing.pendingProps = element.props;
existing.return = returnFiber;
if (__DEV__) {
existing._debugSource = element._source;
@@ -354,7 +357,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
current: Fiber | null,
coroutine: ReactCoroutine,
- priority: PriorityLevel,
): Fiber {
// TODO: Should this also compare handler to determine whether to reuse?
if (current === null || current.tag !== CoroutineComponent) {
@@ -362,14 +364,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromCoroutine(
coroutine,
returnFiber.internalContextTag,
- priority,
);
created.return = returnFiber;
return created;
} else {
// Move based on index
- const existing = useFiber(current, priority);
- existing.pendingProps = coroutine;
+ const existing = useFiber(current, coroutine);
existing.return = returnFiber;
return existing;
}
@@ -379,21 +379,19 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
current: Fiber | null,
yieldNode: ReactYield,
- priority: PriorityLevel,
): Fiber {
if (current === null || current.tag !== YieldComponent) {
// Insert
const created = createFiberFromYield(
yieldNode,
returnFiber.internalContextTag,
- priority,
);
created.type = yieldNode.value;
created.return = returnFiber;
return created;
} else {
// Move based on index
- const existing = useFiber(current, priority);
+ const existing = useFiber(current, emptyObject);
existing.type = yieldNode.value;
existing.return = returnFiber;
return existing;
@@ -404,7 +402,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
current: Fiber | null,
portal: ReactPortal,
- priority: PriorityLevel,
): Fiber {
if (
current === null ||
@@ -416,14 +413,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromPortal(
portal,
returnFiber.internalContextTag,
- priority,
);
created.return = returnFiber;
return created;
} else {
// Update
- const existing = useFiber(current, priority);
- existing.pendingProps = portal.children || [];
+ const existing = useFiber(current, portal.children || []);
existing.return = returnFiber;
return existing;
}
@@ -433,31 +428,24 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
current: Fiber | null,
fragment: Iterable<*>,
- priority: PriorityLevel,
): Fiber {
if (current === null || current.tag !== Fragment) {
// Insert
const created = createFiberFromFragment(
fragment,
returnFiber.internalContextTag,
- priority,
);
created.return = returnFiber;
return created;
} else {
// Update
- const existing = useFiber(current, priority);
- existing.pendingProps = fragment;
+ const existing = useFiber(current, fragment);
existing.return = returnFiber;
return existing;
}
}
- function createChild(
- returnFiber: Fiber,
- newChild: any,
- priority: PriorityLevel,
- ): Fiber | null {
+ function createChild(returnFiber: Fiber, newChild: any): Fiber | null {
if (typeof newChild === 'string' || typeof newChild === 'number') {
// Text nodes doesn't have keys. If the previous node is implicitly keyed
// we can continue to replace it without aborting even if it is not a text
@@ -465,7 +453,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromText(
'' + newChild,
returnFiber.internalContextTag,
- priority,
);
created.return = returnFiber;
return created;
@@ -477,7 +464,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromElement(
newChild,
returnFiber.internalContextTag,
- priority,
);
created.ref = coerceRef(null, newChild);
created.return = returnFiber;
@@ -488,7 +474,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromCoroutine(
newChild,
returnFiber.internalContextTag,
- priority,
);
created.return = returnFiber;
return created;
@@ -498,7 +483,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromYield(
newChild,
returnFiber.internalContextTag,
- priority,
);
created.type = newChild.value;
created.return = returnFiber;
@@ -509,7 +493,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromPortal(
newChild,
returnFiber.internalContextTag,
- priority,
);
created.return = returnFiber;
return created;
@@ -520,7 +503,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromFragment(
newChild,
returnFiber.internalContextTag,
- priority,
);
created.return = returnFiber;
return created;
@@ -536,7 +518,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
oldFiber: Fiber | null,
newChild: any,
- priority: PriorityLevel,
): Fiber | null {
// Update the fiber if the keys match, otherwise return null.
@@ -549,14 +530,14 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
if (key !== null) {
return null;
}
- return updateTextNode(returnFiber, oldFiber, '' + newChild, priority);
+ return updateTextNode(returnFiber, oldFiber, '' + newChild);
}
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);
} else {
return null;
}
@@ -564,7 +545,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
case REACT_COROUTINE_TYPE: {
if (newChild.key === key) {
- return updateCoroutine(returnFiber, oldFiber, newChild, priority);
+ return updateCoroutine(returnFiber, oldFiber, newChild);
} else {
return null;
}
@@ -575,7 +556,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);
} else {
return null;
}
@@ -583,7 +564,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
case REACT_PORTAL_TYPE: {
if (newChild.key === key) {
- return updatePortal(returnFiber, oldFiber, newChild, priority);
+ return updatePortal(returnFiber, oldFiber, newChild);
} else {
return null;
}
@@ -596,7 +577,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
if (key !== null) {
return null;
}
- return updateFragment(returnFiber, oldFiber, newChild, priority);
+ return updateFragment(returnFiber, oldFiber, newChild);
}
throwOnInvalidObjectType(returnFiber, newChild);
@@ -610,13 +591,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
newIdx: number,
newChild: any,
- priority: PriorityLevel,
): 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);
}
if (typeof newChild === 'object' && newChild !== null) {
@@ -626,7 +606,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
existingChildren.get(
newChild.key === null ? newIdx : newChild.key,
) || null;
- return updateElement(returnFiber, matchedFiber, newChild, priority);
+ return updateElement(returnFiber, matchedFiber, newChild);
}
case REACT_COROUTINE_TYPE: {
@@ -634,14 +614,14 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
existingChildren.get(
newChild.key === null ? newIdx : newChild.key,
) || null;
- return updateCoroutine(returnFiber, matchedFiber, newChild, priority);
+ return updateCoroutine(returnFiber, matchedFiber, newChild);
}
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);
}
case REACT_PORTAL_TYPE: {
@@ -649,13 +629,13 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
existingChildren.get(
newChild.key === null ? newIdx : newChild.key,
) || null;
- return updatePortal(returnFiber, matchedFiber, newChild, priority);
+ return updatePortal(returnFiber, matchedFiber, newChild);
}
}
if (isArray(newChild) || getIteratorFn(newChild)) {
const matchedFiber = existingChildren.get(newIdx) || null;
- return updateFragment(returnFiber, matchedFiber, newChild, priority);
+ return updateFragment(returnFiber, matchedFiber, newChild);
}
throwOnInvalidObjectType(returnFiber, newChild);
@@ -713,7 +693,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
- priority: PriorityLevel,
): 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
@@ -757,12 +736,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
} else {
nextOldFiber = oldFiber.sibling;
}
- const newFiber = updateSlot(
- returnFiber,
- oldFiber,
- newChildren[newIdx],
- priority,
- );
+ const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
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
@@ -805,11 +779,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 (; newIdx < newChildren.length; newIdx++) {
- const newFiber = createChild(
- returnFiber,
- newChildren[newIdx],
- priority,
- );
+ const newFiber = createChild(returnFiber, newChildren[newIdx]);
if (!newFiber) {
continue;
}
@@ -835,7 +805,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber,
newIdx,
newChildren[newIdx],
- priority,
);
if (newFiber) {
if (shouldTrackSideEffects) {
@@ -872,7 +841,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildrenIterable: Iterable<*>,
- priority: PriorityLevel,
): Fiber | null {
// This is the same implementation as reconcileChildrenArray(),
// but using the iterator instead.
@@ -936,7 +904,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
} else {
nextOldFiber = oldFiber.sibling;
}
- const newFiber = updateSlot(returnFiber, oldFiber, step.value, priority);
+ const newFiber = updateSlot(returnFiber, oldFiber, step.value);
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
@@ -979,7 +947,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);
if (newFiber === null) {
continue;
}
@@ -1005,7 +973,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber,
newIdx,
step.value,
- priority,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
@@ -1042,7 +1009,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
textContent: string,
- priority: PriorityLevel,
): Fiber {
// There's no need to check for keys on text nodes since we don't have a
// way to define them.
@@ -1050,8 +1016,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);
- existing.pendingProps = textContent;
+ const existing = useFiber(currentFirstChild, textContent);
existing.return = returnFiber;
return existing;
}
@@ -1061,7 +1026,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromText(
textContent,
returnFiber.internalContextTag,
- priority,
);
created.return = returnFiber;
return created;
@@ -1071,7 +1035,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
- priority: PriorityLevel,
): Fiber {
const key = element.key;
let child = currentFirstChild;
@@ -1081,9 +1044,8 @@ 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, element.props);
existing.ref = coerceRef(child, element);
- existing.pendingProps = element.props;
existing.return = returnFiber;
if (__DEV__) {
existing._debugSource = element._source;
@@ -1103,7 +1065,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromElement(
element,
returnFiber.internalContextTag,
- priority,
);
created.ref = coerceRef(currentFirstChild, element);
created.return = returnFiber;
@@ -1114,7 +1075,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
coroutine: ReactCoroutine,
- priority: PriorityLevel,
): Fiber {
const key = coroutine.key;
let child = currentFirstChild;
@@ -1124,8 +1084,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
if (child.key === key) {
if (child.tag === CoroutineComponent) {
deleteRemainingChildren(returnFiber, child.sibling);
- const existing = useFiber(child, priority);
- existing.pendingProps = coroutine;
+ const existing = useFiber(child, coroutine);
existing.return = returnFiber;
return existing;
} else {
@@ -1141,7 +1100,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromCoroutine(
coroutine,
returnFiber.internalContextTag,
- priority,
);
created.return = returnFiber;
return created;
@@ -1151,14 +1109,13 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
yieldNode: ReactYield,
- priority: PriorityLevel,
): 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, emptyObject);
existing.type = yieldNode.value;
existing.return = returnFiber;
return existing;
@@ -1170,7 +1127,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromYield(
yieldNode,
returnFiber.internalContextTag,
- priority,
);
created.type = yieldNode.value;
created.return = returnFiber;
@@ -1181,7 +1137,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
portal: ReactPortal,
- priority: PriorityLevel,
): Fiber {
const key = portal.key;
let child = currentFirstChild;
@@ -1195,8 +1150,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
child.stateNode.implementation === portal.implementation
) {
deleteRemainingChildren(returnFiber, child.sibling);
- const existing = useFiber(child, priority);
- existing.pendingProps = portal.children || [];
+ const existing = useFiber(child, portal.children || []);
existing.return = returnFiber;
return existing;
} else {
@@ -1212,7 +1166,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const created = createFiberFromPortal(
portal,
returnFiber.internalContextTag,
- priority,
);
created.return = returnFiber;
return created;
@@ -1225,7 +1178,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
- priority: PriorityLevel,
): Fiber | null {
// This function is not recursive.
// If the top level item is an array, we treat it as a set of children,
@@ -1234,8 +1186,13 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
const disableNewFiberFeatures = ReactFeatureFlags.disableNewFiberFeatures;
+ if (newChild === null) {
+ // Fast path for null children
+ return deleteRemainingChildren(returnFiber, currentFirstChild);
+ }
+
// Handle object types
- const isObject = typeof newChild === 'object' && newChild !== null;
+ const isObject = typeof newChild === 'object';
if (isObject) {
// Support only the subset of return types that Stack supports. Treat
// everything else as empty, but log a warning.
@@ -1243,34 +1200,19 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
- reconcileSingleElement(
- returnFiber,
- currentFirstChild,
- newChild,
- priority,
- ),
+ reconcileSingleElement(returnFiber, currentFirstChild, newChild),
);
case REACT_PORTAL_TYPE:
return placeSingleChild(
- reconcileSinglePortal(
- returnFiber,
- currentFirstChild,
- newChild,
- priority,
- ),
+ reconcileSinglePortal(returnFiber, currentFirstChild, newChild),
);
}
} else {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
- reconcileSingleElement(
- returnFiber,
- currentFirstChild,
- newChild,
- priority,
- ),
+ reconcileSingleElement(returnFiber, currentFirstChild, newChild),
);
case REACT_COROUTINE_TYPE:
@@ -1279,28 +1221,17 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber,
currentFirstChild,
newChild,
- priority,
),
);
case REACT_YIELD_TYPE:
return placeSingleChild(
- reconcileSingleYield(
- returnFiber,
- currentFirstChild,
- newChild,
- priority,
- ),
+ reconcileSingleYield(returnFiber, currentFirstChild, newChild),
);
case REACT_PORTAL_TYPE:
return placeSingleChild(
- reconcileSinglePortal(
- returnFiber,
- currentFirstChild,
- newChild,
- priority,
- ),
+ reconcileSinglePortal(returnFiber, currentFirstChild, newChild),
);
}
}
@@ -1349,22 +1280,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
if (typeof newChild === 'string' || typeof newChild === 'number') {
return placeSingleChild(
- reconcileSingleTextNode(
- returnFiber,
- currentFirstChild,
- '' + newChild,
- priority,
- ),
+ reconcileSingleTextNode(returnFiber, currentFirstChild, '' + newChild),
);
}
if (isArray(newChild)) {
- return reconcileChildrenArray(
- returnFiber,
- currentFirstChild,
- newChild,
- priority,
- );
+ return reconcileChildrenArray(returnFiber, currentFirstChild, newChild);
}
if (getIteratorFn(newChild)) {
@@ -1372,7 +1293,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
returnFiber,
currentFirstChild,
newChild,
- priority,
);
}
@@ -1422,48 +1342,3 @@ exports.reconcileChildFibers = ChildReconciler(true, true);
exports.reconcileChildFibersInPlace = ChildReconciler(false, true);
exports.mountChildFibersInPlace = ChildReconciler(false, false);
-
-exports.cloneChildFibers = function(
- current: Fiber | null,
- workInProgress: Fiber,
-): void {
- if (!workInProgress.child) {
- return;
- }
- if (current !== null && workInProgress.child === current.child) {
- // We use workInProgress.child since that lets Flow know that it can't be
- // null since we validated that already. However, as the line above suggests
- // they're actually the same thing.
- let currentChild = workInProgress.child;
- // TODO: This used to reset the pending priority. Not sure if that is needed.
- // workInProgress.pendingWorkPriority = current.pendingWorkPriority;
- // TODO: The below priority used to be set to NoWork which would've
- // dropped work. This is currently unobservable but will become
- // observable when the first sibling has lower priority work remaining
- // than the next sibling. At that point we should add tests that catches
- // this.
- let newChild = cloneFiber(currentChild, currentChild.pendingWorkPriority);
- workInProgress.child = newChild;
-
- newChild.return = workInProgress;
- while (currentChild.sibling !== null) {
- currentChild = currentChild.sibling;
- newChild = newChild.sibling = cloneFiber(
- currentChild,
- currentChild.pendingWorkPriority,
- );
- newChild.return = workInProgress;
- }
- newChild.sibling = null;
- } else {
- // If there is no alternate, then we don't need to clone the children.
- // If the children of the alternate fiber is a different set, then we don't
- // need to clone. We need to reset the return fiber though since we'll
- // traverse down into them.
- let child = workInProgress.child;
- while (child !== null) {
- child.return = workInProgress;
- child = child.sibling;
- }
- }
-};
diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js
index 1cebd90a03c1..d40e32276cbb 100644
--- a/src/renderers/shared/fiber/ReactFiber.js
+++ b/src/renderers/shared/fiber/ReactFiber.js
@@ -39,14 +39,13 @@ var {
} = require('ReactTypeOfWork');
var {NoWork} = require('ReactPriorityLevel');
-
+var {getUpdatePriority} = require('ReactFiberUpdateQueue');
var {NoContext} = require('ReactTypeOfInternalContext');
var {NoEffect} = require('ReactTypeOfSideEffect');
-var {cloneUpdateQueue} = require('ReactFiberUpdateQueue');
-
var invariant = require('fbjs/lib/invariant');
+var emptyObject = require('fbjs/lib/emptyObject');
if (__DEV__) {
var getComponentName = require('getComponentName');
@@ -124,7 +123,7 @@ export type Fiber = {
// A queue of state updates and callbacks.
updateQueue: UpdateQueue | null,
- // The state used to create the output
+ // The state used to create the output. TODO: Move this to the update queue?
memoizedState: any,
// Bitfield that describes properties about the fiber and its subtree. E.g.
@@ -146,6 +145,10 @@ export type Fiber = {
// this fiber.
firstEffect: Fiber | null,
lastEffect: Fiber | null,
+ // Child deletion effects are separate so that they can be kept at the front
+ // of the list
+ firstDeletion: Fiber | null,
+ lastDeletion: Fiber | null,
// This will be used to quickly determine if a subtree has no pending changes.
pendingWorkPriority: PriorityLevel,
@@ -154,19 +157,11 @@ export type Fiber = {
// component. This indicates whether it is better to continue from the
// progressed work or if it is better to continue from the current state.
progressedPriority: PriorityLevel,
-
- // If work bails out on a Fiber that already had some work started at a lower
- // priority, then we need to store the progressed work somewhere. This holds
- // the started child set until we need to get back to working on it. It may
- // or may not be the same as the "current" child.
- progressedChild: Fiber | null,
-
- // When we reconcile children onto progressedChild it is possible that we have
- // to delete some child fibers. We need to keep track of this side-effects so
- // that if we continue later on, we have to include those effects. Deletions
- // are added in the reverse order from sibling pointers.
- progressedFirstDeletion: Fiber | null,
- progressedLastDeletion: Fiber | null,
+ progressedWork: ProgressedWork,
+ // A pooled ProgressedWork object, allocated once per fiber pair. Should not
+ // be accessed outside of this module.
+ _pooledProgressedWork: null | ProgressedWork,
+ newestWork: Fiber,
// 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
@@ -178,6 +173,26 @@ export type Fiber = {
// to be the same as work in progress.
};
+// If work bails out on a Fiber that already had some work started at a lower
+// priority, then we need to store the progressed work somewhere. This holds
+// the started child set until we need to get back to working on it, along
+// with the corresponding props and state.
+export type ProgressedWork = {
+ child: Fiber | null,
+ // When we reconcile children onto the progressed child it is possible that we
+ // have to delete some child fibers. We need to keep track of this
+ // side-effects so that if we continue later on, we have to include those
+ // effects. Deletions are added in the reverse order from sibling pointers.
+ firstDeletion: Fiber | null,
+ lastDeletion: Fiber | null,
+
+ memoizedProps: any,
+ memoizedState: any,
+ updateQueue: UpdateQueue | null,
+
+ pendingWorkPriority: PriorityLevel,
+};
+
if (__DEV__) {
var debugCounter = 1;
}
@@ -232,16 +247,22 @@ var createFiber = function(
nextEffect: null,
firstEffect: null,
lastEffect: null,
+ firstDeletion: null,
+ lastDeletion: null,
pendingWorkPriority: NoWork,
+ // TODO: Express this circular reference properly
+ progressedWork: (null: any),
+ _pooledProgressedWork: null,
progressedPriority: NoWork,
- progressedChild: null,
- progressedFirstDeletion: null,
- progressedLastDeletion: null,
+ newestWork: (null: any),
alternate: null,
};
+ fiber.progressedWork = fiber;
+ fiber.newestWork = fiber;
+
if (__DEV__) {
fiber._debugID = debugCounter++;
fiber._debugSource = null;
@@ -259,67 +280,92 @@ function shouldConstruct(Component) {
return !!(Component.prototype && Component.prototype.isReactComponent);
}
-// This is used to create an alternate fiber to do work on.
-// TODO: Rename to createWorkInProgressFiber or something like that.
-exports.cloneFiber = function(
- fiber: Fiber,
- priorityLevel: PriorityLevel,
-): Fiber {
- // We clone to get a work in progress. That means that this fiber is the
- // current. To make it safe to reuse that fiber later on as work in progress
- // we need to reset its work in progress flag now. We don't have an
- // opportunity to do this earlier since we don't traverse the tree when
- // the work in progress tree becomes the current tree.
- // fiber.progressedPriority = NoWork;
- // fiber.progressedChild = null;
+exports.createProgressedWorkFork = function(
+ child: Fiber | null,
+ firstDeletion: Fiber | null,
+ lastDeletion: Fiber | null,
+ memoizedProps: any,
+ memoizedState: any,
+ updateQueue: UpdateQueue | null,
+ pendingWorkPriority: PriorityLevel,
+): ProgressedWork {
+ // ProgressedWork is a subset of Fiber. We use a separate Flow type to prevent
+ // access of extra properties, but we use a whole Fiber for monomorphism.
+ const progressedWork = createFiber(
+ // The actual values here don't matter.
+ HostRoot,
+ null,
+ NoContext,
+ );
+ progressedWork.child = child;
+ progressedWork.firstDeletion = firstDeletion;
+ progressedWork.lastDeletion = lastDeletion;
+ progressedWork.memoizedProps = memoizedProps;
+ progressedWork.memoizedState = memoizedState;
+ progressedWork.updateQueue = updateQueue;
+ progressedWork.pendingWorkPriority = pendingWorkPriority;
+ return progressedWork;
+};
+// This is used to create an alternate fiber to do work on. It's called during
+// the parent's begin phase, either during reconciliation or after the parent
+// bails out.
+exports.createWorkInProgress = function(
+ current: Fiber,
+ pendingProps: mixed,
+): Fiber {
// We use a double buffering pooling technique because we know that we'll only
// ever need at most two versions of a tree. We pool the "other" unused node
// that we're free to reuse. This is lazily created to avoid allocating extra
// objects for things that are never updated. It also allow us to reclaim the
// extra memory if needed.
- let alt = fiber.alternate;
- if (alt !== null) {
- // If we clone, then we do so from the "current" state. The current state
- // can't have any side-effects that are still valid so we reset just to be
- // sure.
- alt.effectTag = NoEffect;
- alt.nextEffect = null;
- alt.firstEffect = null;
- alt.lastEffect = null;
- } else {
- // This should not have an alternate already
- alt = createFiber(fiber.tag, fiber.key, fiber.internalContextTag);
- alt.type = fiber.type;
-
- alt.progressedChild = fiber.progressedChild;
- alt.progressedPriority = fiber.progressedPriority;
+ let workInProgress = current.alternate;
+ if (workInProgress === null) {
+ // No existing alternate. Create a new fiber.
+ workInProgress = createFiber(
+ current.tag,
+ current.key,
+ current.internalContextTag,
+ );
+ workInProgress.type = current.type;
+
+ workInProgress.progressedPriority = current.progressedPriority;
+ workInProgress._pooledProgressedWork = current._pooledProgressedWork;
+ workInProgress.progressedWork = current.progressedWork;
+ workInProgress.newestWork = current.newestWork;
+
+ // Clone child from current.
+ workInProgress.child = current.child;
+ // The deletion list on current is no longer valid.
+ workInProgress.firstDeletion = null;
+ workInProgress.lastDeletion = null;
+ workInProgress.memoizedProps = current.memoizedProps;
+ workInProgress.memoizedState = current.memoizedState;
+ workInProgress.updateQueue = current.updateQueue;
+
+ workInProgress.stateNode = current.stateNode;
+ if (__DEV__) {
+ workInProgress._debugID = current._debugID;
+ workInProgress._debugSource = current._debugSource;
+ workInProgress._debugOwner = current._debugOwner;
+ }
- alt.alternate = fiber;
- fiber.alternate = alt;
+ workInProgress.alternate = current;
+ current.alternate = workInProgress;
}
- alt.stateNode = fiber.stateNode;
- alt.child = fiber.child;
- alt.sibling = fiber.sibling; // This should always be overridden. TODO: null
- alt.index = fiber.index; // This should always be overridden.
- alt.ref = fiber.ref;
- // pendingProps is here for symmetry but is unnecessary in practice for now.
- // TODO: Pass in the new pendingProps as an argument maybe?
- alt.pendingProps = fiber.pendingProps;
- cloneUpdateQueue(fiber, alt);
- alt.pendingWorkPriority = priorityLevel;
+ // Only touch fields that are set by the parent, not fields that correspond to
+ // the child (memoizedProps, memoizedState, et al), which will be determined
+ // during its own begin phase.
+ workInProgress.pendingProps = pendingProps;
- alt.memoizedProps = fiber.memoizedProps;
- alt.memoizedState = fiber.memoizedState;
+ // These are overriden during reconcilation.
+ workInProgress.effectTag = NoEffect;
+ workInProgress.sibling = current.sibling; // This should always be overridden. TODO: null
+ workInProgress.index = current.index; // This should always be overridden.
+ workInProgress.ref = current.ref;
- if (__DEV__) {
- alt._debugID = fiber._debugID;
- alt._debugSource = fiber._debugSource;
- alt._debugOwner = fiber._debugOwner;
- }
-
- return alt;
+ return workInProgress;
};
exports.createHostRootFiber = function(): Fiber {
@@ -330,7 +376,6 @@ exports.createHostRootFiber = function(): Fiber {
exports.createFiberFromElement = function(
element: ReactElement,
internalContextTag: TypeOfInternalContext,
- priorityLevel: PriorityLevel,
): Fiber {
let owner = null;
if (__DEV__) {
@@ -344,7 +389,6 @@ exports.createFiberFromElement = function(
owner,
);
fiber.pendingProps = element.props;
- fiber.pendingWorkPriority = priorityLevel;
if (__DEV__) {
fiber._debugSource = element._source;
@@ -357,24 +401,20 @@ exports.createFiberFromElement = function(
exports.createFiberFromFragment = function(
elements: ReactFragment,
internalContextTag: TypeOfInternalContext,
- priorityLevel: PriorityLevel,
): 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;
return fiber;
};
exports.createFiberFromText = function(
content: string,
internalContextTag: TypeOfInternalContext,
- priorityLevel: PriorityLevel,
): Fiber {
const fiber = createFiber(HostText, null, internalContextTag);
fiber.pendingProps = content;
- fiber.pendingWorkPriority = priorityLevel;
return fiber;
};
@@ -438,6 +478,7 @@ exports.createFiberFromElementType = createFiberFromElementType;
exports.createFiberFromHostInstanceForDeletion = function(): Fiber {
const fiber = createFiber(HostComponent, null, NoContext);
+ fiber.pendingProps = emptyObject;
fiber.type = 'DELETED';
return fiber;
};
@@ -445,7 +486,6 @@ exports.createFiberFromHostInstanceForDeletion = function(): Fiber {
exports.createFiberFromCoroutine = function(
coroutine: ReactCoroutine,
internalContextTag: TypeOfInternalContext,
- priorityLevel: PriorityLevel,
): Fiber {
const fiber = createFiber(
CoroutineComponent,
@@ -454,30 +494,101 @@ exports.createFiberFromCoroutine = function(
);
fiber.type = coroutine.handler;
fiber.pendingProps = coroutine;
- fiber.pendingWorkPriority = priorityLevel;
return fiber;
};
exports.createFiberFromYield = function(
yieldNode: ReactYield,
internalContextTag: TypeOfInternalContext,
- priorityLevel: PriorityLevel,
): Fiber {
const fiber = createFiber(YieldComponent, null, internalContextTag);
+ fiber.pendingProps = emptyObject;
return fiber;
};
exports.createFiberFromPortal = function(
portal: ReactPortal,
internalContextTag: TypeOfInternalContext,
- priorityLevel: PriorityLevel,
): Fiber {
const fiber = createFiber(HostPortal, portal.key, internalContextTag);
fiber.pendingProps = portal.children || [];
- fiber.pendingWorkPriority = priorityLevel;
fiber.stateNode = {
containerInfo: portal.containerInfo,
implementation: portal.implementation,
};
return fiber;
};
+
+// TODO: The remaining functions don't really belong in this module. I just put
+// them here until I figure out what to do with them.
+function largerPriority(p1: PriorityLevel, p2: PriorityLevel): PriorityLevel {
+ return p1 !== NoWork && (p2 === NoWork || p2 > p1) ? p1 : p2;
+}
+exports.largerPriority = largerPriority;
+
+function appendEffects(workInProgress, firstEffect, lastEffect) {
+ if (workInProgress.firstEffect === null) {
+ workInProgress.firstEffect = firstEffect;
+ }
+ if (lastEffect !== null) {
+ if (workInProgress.lastEffect !== null) {
+ workInProgress.lastEffect.nextEffect = firstEffect;
+ }
+ workInProgress.lastEffect = lastEffect;
+ }
+}
+
+exports.getPriorityFromChildren = function(
+ workInProgress: Fiber,
+): PriorityLevel {
+ let priority = NoWork;
+ let child = workInProgress.child;
+ while (child !== null) {
+ priority = largerPriority(priority, child.pendingWorkPriority);
+ priority = largerPriority(priority, getUpdatePriority(child));
+ child = child.sibling;
+ }
+ return priority;
+};
+
+type EffectList = {
+ firstEffect: Fiber | null,
+ lastEffect: Fiber | null,
+};
+
+exports.transferEffectsToParent = function(
+ returnFiber: EffectList,
+ workInProgress: Fiber,
+) {
+ // Append all the effects of the subtree and this fiber onto the effect
+ // list of the parent. The completion order of the children affects the
+ // side-effect order.
+
+ // Append deletions first
+ appendEffects(
+ returnFiber,
+ workInProgress.firstDeletion,
+ workInProgress.lastDeletion,
+ );
+ // Now append the rest of the effect list
+ appendEffects(
+ returnFiber,
+ workInProgress.firstEffect,
+ workInProgress.lastEffect,
+ );
+
+ // If this fiber had side-effects, we append it AFTER the children's
+ // side-effects. We can perform certain side-effects earlier if
+ // needed, by doing multiple passes over the effect list. We don't want
+ // to schedule our own side-effect on our own list because if end up
+ // reusing children we'll schedule this effect onto itself since we're
+ // at the end.
+ if (workInProgress.effectTag !== NoEffect) {
+ if (returnFiber.lastEffect !== null) {
+ returnFiber.lastEffect.nextEffect = workInProgress;
+ } else {
+ returnFiber.firstEffect = workInProgress;
+ }
+ returnFiber.lastEffect = workInProgress;
+ }
+};
diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js
index c042ce8e9263..bc90dee355c1 100644
--- a/src/renderers/shared/fiber/ReactFiberBeginWork.js
+++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js
@@ -12,19 +12,24 @@
'use strict';
+import type {Fiber, ProgressedWork} from 'ReactFiber';
+import type {FiberRoot} from 'ReactFiberRoot';
+import type {UpdateQueue} from 'ReactFiberUpdateQueue';
import type {ReactCoroutine} from 'ReactTypes';
-import type {Fiber} from 'ReactFiber';
import type {HostContext} from 'ReactFiberHostContext';
import type {HydrationContext} from 'ReactFiberHydrationContext';
-import type {FiberRoot} from 'ReactFiberRoot';
import type {HostConfig} from 'ReactFiberReconciler';
import type {PriorityLevel} from 'ReactPriorityLevel';
+var {
+ createWorkInProgress,
+ createProgressedWorkFork,
+ transferEffectsToParent,
+} = require('ReactFiber');
var {
mountChildFibersInPlace,
reconcileChildFibers,
reconcileChildFibersInPlace,
- cloneChildFibers,
} = require('ReactChildFiber');
var {beginUpdateQueue} = require('ReactFiberUpdateQueue');
var ReactTypeOfWork = require('ReactTypeOfWork');
@@ -37,298 +42,533 @@ var {
invalidateContextProvider,
} = require('ReactFiberContext');
var {
- IndeterminateComponent,
- FunctionalComponent,
- ClassComponent,
HostRoot,
+ HostPortal,
HostComponent,
HostText,
- HostPortal,
+ IndeterminateComponent,
+ FunctionalComponent,
+ ClassComponent,
+ Fragment,
CoroutineComponent,
CoroutineHandlerPhase,
YieldComponent,
- Fragment,
} = ReactTypeOfWork;
+var {
+ ClassUpdater,
+ validateClassInstance,
+ callClassInstanceMethod,
+} = require('ReactFiberClassComponent');
var {NoWork, OffscreenPriority} = require('ReactPriorityLevel');
-var {Placement, ContentReset, Err, Ref} = require('ReactTypeOfSideEffect');
-var ReactFiberClassComponent = require('ReactFiberClassComponent');
+var {
+ Placement,
+ Update,
+ ContentReset,
+ Ref,
+ Err,
+ Callback,
+} = require('ReactTypeOfSideEffect');
+var {AsyncUpdates} = require('ReactTypeOfInternalContext');
var {ReactCurrentOwner} = require('ReactGlobalSharedState');
+var ReactFeatureFlags = require('ReactFeatureFlags');
+var ReactInstanceMap = require('ReactInstanceMap');
var invariant = require('fbjs/lib/invariant');
+var shallowEqual = require('fbjs/lib/shallowEqual');
if (__DEV__) {
var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber');
- var {cancelWorkTimer} = require('ReactDebugFiberPerf');
var warning = require('fbjs/lib/warning');
+ var {
+ startPhaseTimer,
+ stopPhaseTimer,
+ cancelWorkTimer,
+ } = require('ReactDebugFiberPerf');
+ var getComponentName = require('getComponentName');
var warnedAboutStatelessRefs = {};
}
-module.exports = function(
- config: HostConfig,
- hostContext: HostContext,
- hydrationContext: HydrationContext,
- scheduleUpdate: (fiber: Fiber, priorityLevel: PriorityLevel) => void,
- getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel,
-) {
- const {
- shouldSetTextContent,
- useSyncScheduling,
- shouldDeprioritizeSubtree,
- } = config;
-
- const {pushHostContext, pushHostContainer} = hostContext;
+function bailoutHiddenChildren(
+ current: Fiber | null,
+ workInProgress: Fiber,
+ nextProps: any | null,
+ nextState: mixed | null,
+ renderPriority: PriorityLevel,
+): Fiber | null {
+ // We didn't reconcile, but before bailing out, we still need to override
+ // the priority of the children in case it's higher than
+ // OffscreenPriority. This can happen when we switch from visible to
+ // hidden, or if setState is called somewhere in the tree.
+ // TODO: It would be better if this tree got its correct priority set
+ // during scheduleUpdate instead because otherwise we'll start a higher
+ // priority reconciliation first before we can get down here. However,
+ // that is a bit tricky since workInProgress and current can have
+ // different "hidden" settings.
+ workInProgress.pendingWorkPriority = OffscreenPriority;
+ return bailout(current, workInProgress, nextProps, null, renderPriority);
+}
- const {
- enterHydrationState,
- resetHydrationState,
- tryToClaimNextHydratableInstance,
- } = hydrationContext;
+function reconcileHiddenChildren(
+ current: Fiber | null,
+ workInProgress: Fiber,
+ nextChildren: any,
+ nextProps: any | null,
+ nextState: mixed | null,
+ renderPriority: PriorityLevel,
+): Fiber | null {
+ if (renderPriority !== OffscreenPriority) {
+ // This is a special case where we're about to reconcile at a lower
+ // priority than the render priority. We already called resumeOrResetWork
+ // at the start of the begin phase, but we need to call it again with
+ // OffscreenPriority so that if we have an offscreen child, we can
+ // reuse it.
+ resumeOrResetWork(current, workInProgress, OffscreenPriority);
+ }
- const {
- adoptClassInstance,
- constructClassInstance,
- mountClassInstance,
- resumeMountClassInstance,
- updateClassInstance,
- } = ReactFiberClassComponent(
- scheduleUpdate,
- getPriorityContext,
- memoizeProps,
- memoizeState,
+ // Reconcile the children at OffscreenPriority. This may be lower than
+ // the priority at which we're currently reconciling. This will store
+ // the children on the progressed work so that we can come back to them
+ // later if needed.
+ reconcile(
+ current,
+ workInProgress,
+ nextChildren,
+ nextProps,
+ nextState,
+ OffscreenPriority,
);
- function markChildAsProgressed(current, workInProgress, priorityLevel) {
- // We now have clones. Let's store them as the currently progressed work.
- workInProgress.progressedChild = workInProgress.child;
- workInProgress.progressedPriority = priorityLevel;
- if (current !== null) {
- // We also store it on the current. When the alternate swaps in we can
- // continue from this point.
- current.progressedChild = workInProgress.progressedChild;
- current.progressedPriority = workInProgress.progressedPriority;
+ // If we're rendering at OffscreenPriority, start working on the child.
+ if (renderPriority === OffscreenPriority) {
+ return workInProgress.child;
+ }
+
+ // Otherwise, bailout.
+ if (current === null) {
+ // If this doesn't have a current we won't track it for placement
+ // effects. However, when we come back around to this we have already
+ // inserted the parent which means that we'll infact need to make this a
+ // placement.
+ // TODO: There has to be a better solution to this problem.
+ let child = workInProgress.child;
+ while (child !== null) {
+ child.effectTag = Placement;
+ child = child.sibling;
}
}
- function clearDeletions(workInProgress) {
- workInProgress.progressedFirstDeletion = workInProgress.progressedLastDeletion = null;
+ // Usually we update the newestWork at the end of the begin phase. But in this
+ // case, we're doing a reconcile followed immediately by a bailout. So we
+ // need to update newestWork here so that it's correct by the time we bailout.
+ workInProgress.newestWork = workInProgress;
+ if (current !== null) {
+ workInProgress.newestWork = workInProgress;
}
- function transferDeletions(workInProgress) {
- // Any deletions get added first into the effect list.
- workInProgress.firstEffect = workInProgress.progressedFirstDeletion;
- workInProgress.lastEffect = workInProgress.progressedLastDeletion;
+ resumeOrResetWork(current, workInProgress, renderPriority);
+
+ // This will stash the child on a progressed work fork and reset to current.
+ bailout(current, workInProgress, nextProps, nextState, renderPriority);
+
+ // Even though we're bailing out, we actually did complete the work at this
+ // priority. Update the memoized inputs so we can reuse it later.
+ // TODO: Is there a better way to model this? A bit confusing. Or maybe
+ // just a better explanation here would suffice.
+ workInProgress.memoizedProps = nextProps;
+ workInProgress.memoizedState = nextState;
+
+ return null;
+}
+
+function bailout(
+ current: Fiber | null,
+ workInProgress: Fiber,
+ nextProps: any | null,
+ nextState: mixed | null,
+ renderPriority: PriorityLevel,
+): Fiber | null {
+ if (__DEV__) {
+ cancelWorkTimer(workInProgress);
}
- function reconcileChildren(current, workInProgress, nextChildren) {
- const priorityLevel = workInProgress.pendingWorkPriority;
- reconcileChildrenAtPriority(
- current,
- workInProgress,
- nextChildren,
- priorityLevel,
- );
+ // A bailout implies that the memoized props and state are equal to the next
+ // props and state, but we should update them anyway because they might not
+ // be referentially equal (shouldComponentUpdate -> false)
+ workInProgress.memoizedProps = nextProps;
+ workInProgress.memoizedState = nextState;
+
+ // If the child is null, this is terminal. The work is done.
+ if (workInProgress.child === null) {
+ return null;
}
- function reconcileChildrenAtPriority(
+ // Should we continue working on the children? Check if the children have
+ // work that matches the priority at which we're currently rendering.
+ if (
+ workInProgress.pendingWorkPriority === NoWork ||
+ workInProgress.pendingWorkPriority > renderPriority
+ ) {
+ // The children do not have sufficient priority. Return null to skip the
+ // children and continue on the sibling. If there's still work in the
+ // children, we'll come back to it later at a lower priority.
+
+ if (
+ current === null ||
+ current.child === null ||
+ workInProgress.child !== current.child
+ ) {
+ // We have progressed work that completed at this level. Because the
+ // remaining priority (pendingWorkPriority) is less than the priority
+ // at which it last rendered (progressedPriority), we know that it
+ // must have completed at the progressedPriority. That means we can
+ // use the progressed child during this commit.
+
+ // We need to bubble up effects from the progressed children so that
+ // they don't get dropped. Usually effects are transferred to the
+ // parent during the complete phase, but we won't be completing these
+ // children again.
+ let child = workInProgress.child;
+ while (child !== null) {
+ transferEffectsToParent(workInProgress, child);
+ child = child.sibling;
+ }
+ }
+ return null;
+ }
+
+ // The children have pending work that matches the render priority. Continue
+ // on the work-in-progress children.
+ if (
+ current === null ||
+ current.child === null ||
+ workInProgress.child !== current.child
+ ) {
+ // The child is not the current child, which means they are a work-in-
+ // progress set. We can reuse them. But reset the child pointer before
+ // traversing into them so we can find our way back later.
+ let child = workInProgress.child;
+ while (child !== null) {
+ child.return = workInProgress;
+ child = child.sibling;
+ }
+ } else {
+ // The child is the current child. Switch to the work-in-progress set
+ // instead. If a child does not already have a work-in-progress copy,
+ // it will be created.
+ let currentChild = workInProgress.child;
+ let newChild = createWorkInProgress(currentChild, null);
+ workInProgress.child = newChild;
+
+ newChild.return = workInProgress;
+ while (currentChild.sibling !== null) {
+ currentChild = currentChild.sibling;
+ newChild = newChild.sibling = createWorkInProgress(currentChild, null);
+ newChild.return = workInProgress;
+ }
+ newChild.sibling = null;
+
+ // We mutated the child fiber. Mark it as progressed. If we had lower-
+ // priority progressed work, it will be thrown out.
+ markWorkAsProgressed(current, workInProgress, renderPriority);
+ }
+
+ // Continue working on child
+ return workInProgress.child;
+}
+exports.bailout = bailout;
+
+function reconcile(
+ current: Fiber | null,
+ workInProgress: Fiber,
+ nextChildren: any,
+ nextProps: any | null,
+ nextState: mixed | null,
+ renderPriority: PriorityLevel,
+) {
+ const child = (workInProgress.child = reconcileImpl(
current,
workInProgress,
+ workInProgress.child,
nextChildren,
- priorityLevel,
- ) {
- // At this point any memoization is no longer valid since we'll have changed
- // the children.
- workInProgress.memoizedProps = null;
- if (current === null) {
+ nextProps,
+ nextState,
+ false,
+ renderPriority,
+ ));
+ return child;
+}
+exports.reconcile = reconcile;
+
+// Lower level version of reconcile function with more options for special
+// cases. My thinking is that, for now, this is preferable to forking, because
+// it's really easy to mess up when keeping the forks in sync.
+// TODO: Unify this with reconcileChildFibers constructor?
+function reconcileImpl(
+ current: Fiber | null,
+ workInProgress: Fiber,
+ child: Fiber | null, // Child to reconcile against
+ nextChildren: any,
+ nextProps: any | null,
+ nextState: mixed | null,
+ forceMountInPlace: boolean,
+ renderPriority: PriorityLevel,
+): Fiber | null {
+ // We have new children. Update the child set.
+ let newChild;
+ if (forceMountInPlace) {
+ newChild = mountChildFibersInPlace(workInProgress, child, nextChildren);
+ } else if (current === null) {
+ if (workInProgress.tag === HostPortal) {
+ // Portals are special because we don't append the children during mount
+ // but at commit. Therefore we need to track insertions which the normal
+ // flow doesn't do during mount. This doesn't happen at the root because
+ // the root always starts with a "current" with a null child.
+ // TODO: Consider unifying this with how the root works.
+ newChild = reconcileChildFibersInPlace(
+ workInProgress,
+ child,
+ nextChildren,
+ );
+ } else {
// If this is a fresh new component that hasn't been rendered yet, we
// won't update its child set by applying minimal side-effects. Instead,
// we will add them all to the child before it gets rendered. That means
// we can optimize this reconciliation pass by not tracking side-effects.
- workInProgress.child = mountChildFibersInPlace(
- workInProgress,
- workInProgress.child,
- nextChildren,
- priorityLevel,
- );
- } else if (current.child === workInProgress.child) {
- // If the current child is the same as the work in progress, it means that
- // we haven't yet started any work on these children. Therefore, we use
- // the clone algorithm to create a copy of all the current children.
+ newChild = mountChildFibersInPlace(workInProgress, child, nextChildren);
+ }
+ } else if (workInProgress.child === current.child) {
+ // If the child is the same as the current child, it means that we haven't
+ // yet started any work on these children. Therefore, we use the clone
+ // algorithm to create a copy of all the current children.
+ // Note: Compare to `workInProgress.child`, not `child`, because for
+ // a phase one coroutine, `child` is actually the state node.
+ newChild = reconcileChildFibers(workInProgress, child, nextChildren);
+ } else {
+ // If, on the other hand, it is already using a clone, that means we've
+ // already begun some work on this tree and we can continue where we left
+ // off by reconciling against the existing children.
+ newChild = reconcileChildFibersInPlace(workInProgress, child, nextChildren);
+ }
- // If we had any progressed work already, that is invalid at this point so
- // let's throw it out.
- clearDeletions(workInProgress);
+ // Memoize this work.
+ workInProgress.memoizedProps = nextProps;
+ workInProgress.memoizedState = nextState;
- workInProgress.child = reconcileChildFibers(
- workInProgress,
- workInProgress.child,
- nextChildren,
- priorityLevel,
- );
+ // The child is now the progressed child. Update the progressed work.
+ markWorkAsProgressed(current, workInProgress, renderPriority);
- transferDeletions(workInProgress);
- } else {
- // If, on the other hand, it is already using a clone, that means we've
- // already begun some work on this tree and we can continue where we left
- // off by reconciling against the existing children.
- workInProgress.child = reconcileChildFibersInPlace(
- workInProgress,
- workInProgress.child,
- nextChildren,
- priorityLevel,
- );
+ // We reconciled the children set. They now have pending work at whatever
+ // priority we're currently rendering. This is true even if the render
+ // priority is less than the existing work priority, since that should only
+ // happen in the case of an intentional down-prioritization.
+ workInProgress.pendingWorkPriority = renderPriority;
- transferDeletions(workInProgress);
- }
- markChildAsProgressed(current, workInProgress, priorityLevel);
- }
+ return newChild;
+}
- function updateFragment(current, workInProgress) {
- var nextChildren = workInProgress.pendingProps;
- if (hasContextChanged()) {
- // Normally we can bail out on props equality but if context has changed
- // we don't do the bailout and we have to reuse existing props instead.
- if (nextChildren === null) {
- nextChildren = workInProgress.memoizedProps;
- }
- } else if (
- nextChildren === null ||
- workInProgress.memoizedProps === nextChildren
- ) {
- return bailoutOnAlreadyFinishedWork(current, workInProgress);
- }
- reconcileChildren(current, workInProgress, nextChildren);
- memoizeProps(workInProgress, nextChildren);
- return workInProgress.child;
+function markWorkAsProgressed(current, workInProgress, renderPriority) {
+ // Keep track of the priority at which this work was performed.
+ workInProgress.progressedPriority = renderPriority;
+ workInProgress.progressedWork = workInProgress;
+ if (current !== null) {
+ // Set the progressed work on both fibers
+ current.progressedPriority = renderPriority;
+ current.progressedWork = workInProgress;
}
+}
- function markRef(current: Fiber | null, workInProgress: Fiber) {
- const ref = workInProgress.ref;
- if (ref !== null && (!current || current.ref !== ref)) {
- // Schedule a Ref effect
- workInProgress.effectTag |= Ref;
- }
+function resumeAlreadyProgressedWork(
+ workInProgress: Fiber,
+ progressedWork: ProgressedWork,
+) {
+ // Reuse the progressed work.
+ if (progressedWork === workInProgress) {
+ // The work-in-progress is already the same as the progressed work.
+ return;
}
- function updateFunctionalComponent(current, workInProgress) {
- var fn = workInProgress.type;
- var nextProps = workInProgress.pendingProps;
+ // We're resuming off a fork, so we need to update the progressed work
+ // priority, too. The remaining work priority is equal to whatever priority
+ // we interrupted.
+
+ workInProgress.child = progressedWork.child;
+ workInProgress.firstDeletion = progressedWork.firstDeletion;
+ workInProgress.lastDeletion = progressedWork.lastDeletion;
+ workInProgress.memoizedProps = progressedWork.memoizedProps;
+ workInProgress.memoizedState = progressedWork.memoizedState;
+ workInProgress.updateQueue = progressedWork.updateQueue;
+ workInProgress.pendingWorkPriority = progressedWork.pendingWorkPriority;
+
+ // "Un-fork" the progressed work object. We no longer need it.
+ workInProgress.progressedWork = workInProgress;
+ const current = workInProgress.alternate;
+ if (current !== null) {
+ current.progressedWork = workInProgress;
+ }
+}
- const memoizedProps = workInProgress.memoizedProps;
- if (hasContextChanged()) {
- // Normally we can bail out on props equality but if context has changed
- // we don't do the bailout and we have to reuse existing props instead.
- if (nextProps === null) {
- nextProps = memoizedProps;
- }
+function getWorkProgressedSinceLastCommit(
+ current: Fiber | null,
+ workInProgress: Fiber,
+): ProgressedWork | null {
+ const progressedWork = workInProgress.progressedWork;
+ if (progressedWork === workInProgress) {
+ if (workInProgress === workInProgress.newestWork) {
+ // The work-in-progress fiber has work that is newer than the
+ // current fiber.
+ return workInProgress;
} else {
- if (nextProps === null || memoizedProps === nextProps) {
- return bailoutOnAlreadyFinishedWork(current, workInProgress);
- }
- // TODO: Disable this before release, since it is not part of the public API
- // I use this for testing to compare the relative overhead of classes.
- if (
- typeof fn.shouldComponentUpdate === 'function' &&
- !fn.shouldComponentUpdate(memoizedProps, nextProps)
- ) {
- // Memoize props even if shouldComponentUpdate returns false
- memoizeProps(workInProgress, nextProps);
- return bailoutOnAlreadyFinishedWork(current, workInProgress);
- }
+ // The work-in-progress is older than the current fiber. In other words,
+ // not a true work-in-progress, but the "previous current" fiber.
+ return null;
}
+ } else if (progressedWork === current) {
+ // There's been no work since the last commit.
+ return null;
+ } else {
+ // We have stashed work.
+ return progressedWork;
+ }
+}
- var unmaskedContext = getUnmaskedContext(workInProgress);
- var context = getMaskedContext(workInProgress, unmaskedContext);
-
- var nextChildren;
-
- if (__DEV__) {
- ReactCurrentOwner.current = workInProgress;
- ReactDebugCurrentFiber.phase = 'render';
- nextChildren = fn(nextProps, context);
- ReactDebugCurrentFiber.phase = null;
- } else {
- nextChildren = fn(nextProps, context);
- }
- reconcileChildren(current, workInProgress, nextChildren);
- memoizeProps(workInProgress, nextProps);
- return workInProgress.child;
+function forkWorkInProgress(
+ current: Fiber | null,
+ workInProgress: Fiber,
+ child: Fiber | null,
+ firstDeletion: Fiber | null,
+ lastDeletion: Fiber | null,
+ memoizedProps: any,
+ memoizedState: any,
+ updateQueue: UpdateQueue | null,
+ pendingWorkPriority: PriorityLevel,
+) {
+ const stashedWork = createProgressedWorkFork(
+ child,
+ firstDeletion,
+ lastDeletion,
+ memoizedProps,
+ memoizedState,
+ updateQueue,
+ pendingWorkPriority,
+ );
+ workInProgress.progressedWork = stashedWork;
+ if (current !== null) {
+ // Set it on both fibers
+ current.progressedWork = stashedWork;
}
+}
- function updateClassComponent(
- current: Fiber | null,
- workInProgress: Fiber,
- priorityLevel: PriorityLevel,
- ) {
- // 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.
- // We will invalidate the child context in finishClassComponent() right after rendering.
- const hasContext = pushContextProvider(workInProgress);
+function resetToCurrent(
+ current: Fiber | null,
+ workInProgress: Fiber,
+ renderPriority,
+): void {
+ if (current !== null) {
+ // Clone child from current, along with associated fields.
+ workInProgress.child = current.child;
+ // The deletion list on current is no longer valid.
+ workInProgress.firstDeletion = null;
+ workInProgress.lastDeletion = null;
+ workInProgress.memoizedProps = current.memoizedProps;
+ workInProgress.memoizedState = current.memoizedState;
+ workInProgress.updateQueue = current.updateQueue;
+ workInProgress.pendingWorkPriority = current.pendingWorkPriority;
+
+ // The effect list can be left alone because we reset it at the start of
+ // the begin phase. Otherwise we'd reset it to null here, since effects
+ // on the current tree are no longer valid.
+
+ // The remaining fields belong to the "instance" and are always kept in
+ // sync on both copies of the fiber, so we don't need to copy them here.
+ } else {
+ // There is no current, so conceptually, the current fiber is null.
+ workInProgress.child = null;
+ workInProgress.firstDeletion = null;
+ workInProgress.lastDeletion = null;
+ workInProgress.memoizedProps = null;
+ workInProgress.memoizedState = null;
+ workInProgress.updateQueue = null;
+ workInProgress.pendingWorkPriority = NoWork;
+ }
+}
- let shouldUpdate;
- if (current === null) {
- if (!workInProgress.stateNode) {
- // In the initial pass we might need to construct the instance.
- constructClassInstance(workInProgress, workInProgress.pendingProps);
- mountClassInstance(workInProgress, priorityLevel);
- shouldUpdate = true;
- } else {
- // In a resume, we'll already have an instance we can reuse.
- shouldUpdate = resumeMountClassInstance(workInProgress, priorityLevel);
- }
+function resumeOrResetWork(
+ current: Fiber | null,
+ workInProgress: Fiber,
+ renderPriority: PriorityLevel,
+): void {
+ // See if there's any work that has progressed since the last commit
+ const progressedWork = getWorkProgressedSinceLastCommit(
+ current,
+ workInProgress,
+ );
+ if (progressedWork !== null) {
+ // Check to make sure the work progressed at the same priority
+ if (workInProgress.progressedPriority === renderPriority) {
+ resumeAlreadyProgressedWork(workInProgress, progressedWork);
} else {
- shouldUpdate = updateClassInstance(
- current,
- workInProgress,
- priorityLevel,
- );
+ // The work progressed at a different priority, so we can't resume on
+ // it. But we should stash it away so we can come back to it later at
+ // the lower priority.
+ if (progressedWork === workInProgress) {
+ forkWorkInProgress(
+ current,
+ workInProgress,
+ workInProgress.child,
+ workInProgress.firstDeletion,
+ workInProgress.lastDeletion,
+ workInProgress.memoizedProps,
+ workInProgress.memoizedState,
+ workInProgress.updateQueue,
+ workInProgress.pendingWorkPriority,
+ );
+ }
+ // Reset the work-in-progress. This makes it a copy of the current fiber.
+ resetToCurrent(current, workInProgress, renderPriority);
}
- return finishClassComponent(
- current,
- workInProgress,
- shouldUpdate,
- hasContext,
- );
+ } else {
+ // If there's no work since the last commit, reset to current.
+ resetToCurrent(current, workInProgress, renderPriority);
}
+}
- function finishClassComponent(
- current: Fiber | null,
- workInProgress: Fiber,
- shouldUpdate: boolean,
- hasContext: boolean,
- ) {
- // Refs should update even if shouldComponentUpdate returns false
- markRef(current, workInProgress);
-
- if (!shouldUpdate) {
- return bailoutOnAlreadyFinishedWork(current, workInProgress);
- }
+const BeginWork = function(
+ config: HostConfig,
+ hostContext: HostContext,
+ hydrationContext: HydrationContext,
+ scheduleUpdate: (fiber: Fiber, priorityLevel: PriorityLevel) => void,
+ getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel,
+) {
+ const {
+ shouldSetTextContent,
+ useSyncScheduling,
+ shouldDeprioritizeSubtree,
+ } = config;
- const instance = workInProgress.stateNode;
+ const {pushHostContext, pushHostContainer} = hostContext;
+ const classUpdater = ClassUpdater(scheduleUpdate, getPriorityContext);
- // Rerender
- ReactCurrentOwner.current = workInProgress;
- let nextChildren;
- if (__DEV__) {
- ReactDebugCurrentFiber.phase = 'render';
- nextChildren = instance.render();
- ReactDebugCurrentFiber.phase = null;
- } else {
- nextChildren = instance.render();
- }
- reconcileChildren(current, workInProgress, nextChildren);
- // Memoize props and state using the values we just used to render.
- // TODO: Restructure so we never read values from the instance.
- memoizeState(workInProgress, instance.state);
- memoizeProps(workInProgress, instance.props);
+ const {
+ enterHydrationState,
+ resetHydrationState,
+ tryToClaimNextHydratableInstance,
+ } = hydrationContext;
- // The context might have changed so we need to recalculate it.
- if (hasContext) {
- invalidateContextProvider(workInProgress);
+ function checkForUpdatedRef(current: Fiber | null, workInProgress: Fiber) {
+ const ref = workInProgress.ref;
+ if (ref !== null && (current === null || current.ref !== ref)) {
+ // We have a new or updated ref. Schedule a Ref effect so that it
+ // gets attached during the commit phase.
+ workInProgress.effectTag |= Ref;
}
- return workInProgress.child;
}
- function updateHostRoot(current, workInProgress, priorityLevel) {
+ function beginHostRoot(
+ current: Fiber | null,
+ workInProgress: Fiber,
+ nextProps: any,
+ renderPriority: PriorityLevel,
+ ): Fiber | null {
const root = (workInProgress.stateNode: FiberRoot);
if (root.pendingContext) {
pushTopLevelContextObject(
@@ -343,106 +583,149 @@ module.exports = function(
pushHostContainer(workInProgress, root.containerInfo);
+ const memoizedState = workInProgress.memoizedState;
const updateQueue = workInProgress.updateQueue;
- if (updateQueue !== null) {
- const prevState = workInProgress.memoizedState;
- const state = beginUpdateQueue(
+ const nextState = updateQueue === null
+ ? memoizedState
+ : beginUpdateQueue(
+ current,
+ workInProgress,
+ updateQueue,
+ null,
+ memoizedState,
+ null,
+ renderPriority,
+ );
+
+ // Schedule a callback effect if needed.
+ if (
+ workInProgress.updateQueue !== null &&
+ workInProgress.updateQueue.callbackList !== null
+ ) {
+ workInProgress.effectTag |= Callback;
+ }
+
+ if (nextState === memoizedState) {
+ // No new state. The root doesn't have props. Bailout.
+ // TODO: What about context?
+ resetHydrationState();
+ return bailout(
+ current,
workInProgress,
- updateQueue,
- null,
- prevState,
null,
- priorityLevel,
+ memoizedState,
+ renderPriority,
);
- if (prevState === state) {
- // If the state is the same as before, that's a bailout because we had
- // no work matching this priority.
- resetHydrationState();
- return bailoutOnAlreadyFinishedWork(current, workInProgress);
- }
- const element = state.element;
- if (current === null || current.child === null) {
- // If we don't have any current children this might be the first pass.
- // We always try to hydrate. If this isn't a hydration pass there won't
- // be any children to hydrate which is effectively the same thing as
- // not hydrating.
- if (enterHydrationState(workInProgress)) {
- // This is a bit of a hack. We track the host root as a placement to
- // know that we're currently in a mounting state. That way isMounted
- // works as expected. We must reset this before committing.
- // TODO: Delete this when we delete isMounted and findDOMNode.
- workInProgress.effectTag |= Placement;
-
- // Ensure that children mount into this root without tracking
- // side-effects. This ensures that we don't store Placement effects on
- // nodes that will be hydrated.
- workInProgress.child = mountChildFibersInPlace(
- workInProgress,
- workInProgress.child,
- element,
- priorityLevel,
- );
- markChildAsProgressed(current, workInProgress, priorityLevel);
- return workInProgress.child;
- }
- }
- // Otherwise reset hydration state in case we aborted and resumed another
- // root.
+ }
+
+ // The state was updated. We have a new element.
+ const nextChildren = nextState.element;
+
+ // If we don't have any current children this might be the first pass.
+ // We always try to hydrate. If this isn't a hydration pass there won't
+ // be any children to hydrate which is effectively the same thing as
+ // not hydrating.
+ let forceMountInPlace;
+ if (
+ (current === null || current.child === null) &&
+ enterHydrationState(workInProgress)
+ ) {
+ // Ensure that children mount into this root without tracking
+ // side-effects. This ensures that we don't store Placement effects on
+ // nodes that will be hydrated.
+ forceMountInPlace = true;
+
+ // This is a bit of a hack. We track the host root as a placement to
+ // know that we're currently in a mounting state. That way isMounted
+ // works as expected. We must reset this before committing.
+ // TODO: Delete this when we delete isMounted and findDOMNode.
+ workInProgress.effectTag |= Placement;
+ } else {
+ // Otherwise, reset the hydration state
resetHydrationState();
- reconcileChildren(current, workInProgress, element);
- memoizeState(workInProgress, state);
- return workInProgress.child;
+ forceMountInPlace = false;
}
- resetHydrationState();
- // If there is no update queue, that's a bailout because the root has no props.
- return bailoutOnAlreadyFinishedWork(current, workInProgress);
+
+ // Reconcile the children.
+ const child = (workInProgress.child = reconcileImpl(
+ current,
+ workInProgress,
+ workInProgress.child,
+ nextChildren,
+ null,
+ nextState,
+ forceMountInPlace,
+ renderPriority,
+ ));
+ return child;
}
- function updateHostComponent(current, workInProgress) {
+ function beginHostPortal(
+ current: Fiber | null,
+ workInProgress: Fiber,
+ nextChildren: mixed,
+ renderPriority: PriorityLevel,
+ ): Fiber | null {
+ pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo);
+
+ const memoizedChildren = workInProgress.memoizedProps;
+ if (nextChildren === memoizedChildren && !hasContextChanged()) {
+ return bailout(
+ current,
+ workInProgress,
+ nextChildren,
+ null,
+ renderPriority,
+ );
+ }
+
+ // Reconcile the children.
+ return reconcile(
+ current,
+ workInProgress,
+ nextChildren,
+ nextChildren,
+ null,
+ renderPriority,
+ );
+ }
+
+ function beginHostComponent(
+ current: Fiber | null,
+ workInProgress: Fiber,
+ nextProps: any,
+ renderPriority: PriorityLevel,
+ ): Fiber | null {
+ const type = workInProgress.type;
+
pushHostContext(workInProgress);
if (current === null) {
tryToClaimNextHydratableInstance(workInProgress);
}
- let nextProps = workInProgress.pendingProps;
- const type = workInProgress.type;
- const prevProps = current !== null ? current.memoizedProps : null;
const memoizedProps = workInProgress.memoizedProps;
- if (hasContextChanged()) {
- // Normally we can bail out on props equality but if context has changed
- // we don't do the bailout and we have to reuse existing props instead.
- if (nextProps === null) {
- nextProps = memoizedProps;
- invariant(
- nextProps !== null,
- 'We should always have pending or current props. This error is ' +
- 'likely caused by a bug in React. Please file an issue.',
+
+ // Check if the ref has changed and schedule an effect. This should happen
+ // even if we bailout.
+ checkForUpdatedRef(current, workInProgress);
+
+ // Check the host config to see if the children are offscreen/hidden.
+ const isHidden =
+ !useSyncScheduling && shouldDeprioritizeSubtree(type, nextProps);
+
+ if (nextProps === memoizedProps && !hasContextChanged()) {
+ // Neither props nor context changed. Bailout.
+ if (isHidden) {
+ return bailoutHiddenChildren(
+ current,
+ workInProgress,
+ nextProps,
+ null,
+ renderPriority,
);
}
- } else if (nextProps === null || memoizedProps === nextProps) {
- if (
- !useSyncScheduling &&
- shouldDeprioritizeSubtree(type, memoizedProps) &&
- workInProgress.pendingWorkPriority !== OffscreenPriority
- ) {
- // This subtree still has work, but it should be deprioritized so we need
- // to bail out and not do any work yet.
- // TODO: It would be better if this tree got its correct priority set
- // during scheduleUpdate instead because otherwise we'll start a higher
- // priority reconciliation first before we can get down here. However,
- // that is a bit tricky since workInProgress and current can have
- // different "hidden" settings.
- let child = workInProgress.progressedChild;
- while (child !== null) {
- // To ensure that this subtree gets its priority reset, the children
- // need to be reset.
- child.pendingWorkPriority = OffscreenPriority;
- child = child.sibling;
- }
- return null;
- }
- return bailoutOnAlreadyFinishedWork(current, workInProgress);
+ return bailout(current, workInProgress, nextProps, null, renderPriority);
}
let nextChildren = nextProps.children;
@@ -454,97 +737,84 @@ module.exports = function(
// this in the host environment that also have access to this prop. That
// avoids allocating another HostText fiber and traversing it.
nextChildren = null;
- } else if (prevProps && shouldSetTextContent(type, prevProps)) {
+ } else if (
+ memoizedProps != null &&
+ shouldSetTextContent(type, memoizedProps)
+ ) {
// If we're switching from a direct text child to a normal child, or to
// empty, we need to schedule the text content to be reset.
workInProgress.effectTag |= ContentReset;
}
- markRef(current, workInProgress);
-
- if (
- !useSyncScheduling &&
- shouldDeprioritizeSubtree(workInProgress.type, nextProps) &&
- workInProgress.pendingWorkPriority !== OffscreenPriority
- ) {
- // If this host component is hidden, we can bail out on the children.
- // We'll rerender the children later at the lower priority.
-
- // It is unfortunate that we have to do the reconciliation of these
- // children already since that will add them to the tree even though
- // they are not actually done yet. If this is a large set it is also
- // confusing that this takes time to do right now instead of later.
-
- if (workInProgress.progressedPriority === OffscreenPriority) {
- // If we already made some progress on the offscreen priority before,
- // then we should continue from where we left off.
- workInProgress.child = workInProgress.progressedChild;
- }
-
- // Reconcile the children and stash them for later work.
- reconcileChildrenAtPriority(
+ if (isHidden) {
+ return reconcileHiddenChildren(
current,
workInProgress,
nextChildren,
- OffscreenPriority,
+ nextProps,
+ null,
+ renderPriority,
);
- memoizeProps(workInProgress, nextProps);
- workInProgress.child = current !== null ? current.child : null;
-
- if (current === null) {
- // If this doesn't have a current we won't track it for placement
- // effects. However, when we come back around to this we have already
- // inserted the parent which means that we'll infact need to make this a
- // placement.
- // TODO: There has to be a better solution to this problem.
- let child = workInProgress.progressedChild;
- while (child !== null) {
- child.effectTag = Placement;
- child = child.sibling;
- }
- }
-
- // Abort and don't process children yet.
- return null;
- } else {
- reconcileChildren(current, workInProgress, nextChildren);
- memoizeProps(workInProgress, nextProps);
- return workInProgress.child;
}
+ return reconcile(
+ current,
+ workInProgress,
+ nextChildren,
+ nextProps,
+ null,
+ renderPriority,
+ );
}
- function updateHostText(current, workInProgress) {
+ function beginHostText(
+ current: Fiber | null,
+ workInProgress: Fiber,
+ nextProps: any,
+ renderPriority: PriorityLevel,
+ ): Fiber | null {
if (current === null) {
tryToClaimNextHydratableInstance(workInProgress);
}
- let nextProps = workInProgress.pendingProps;
- if (nextProps === null) {
- nextProps = workInProgress.memoizedProps;
- }
- memoizeProps(workInProgress, nextProps);
- // Nothing to do here. This is terminal. We'll do the completion step
- // immediately after.
- return null;
+
+ const memoizedProps = workInProgress.memoizedProps;
+ if (nextProps === memoizedProps) {
+ return bailout(current, workInProgress, nextProps, null, renderPriority);
+ }
+ // Text nodes don't actually have children, but we call reconcile anyway
+ // so that the progressed work gets updated.
+ return reconcile(
+ current,
+ workInProgress,
+ null,
+ nextProps,
+ null,
+ renderPriority,
+ );
}
- function mountIndeterminateComponent(current, workInProgress, priorityLevel) {
+ function beginIndeterminateComponent(
+ current: Fiber | null,
+ workInProgress: Fiber,
+ nextProps: any,
+ renderPriority: PriorityLevel,
+ ): Fiber | null {
invariant(
current === null,
'An indeterminate component should never have mounted. This error is ' +
'likely caused by a bug in React. Please file an issue.',
);
- var fn = workInProgress.type;
- var props = workInProgress.pendingProps;
- var unmaskedContext = getUnmaskedContext(workInProgress);
- var context = getMaskedContext(workInProgress, unmaskedContext);
- var value;
+ const fn = workInProgress.type;
+ let unmaskedContext = getUnmaskedContext(workInProgress);
+ let nextContext = getMaskedContext(workInProgress, unmaskedContext);
+ // This is either a functional component or a module-style class component.
+ let value;
if (__DEV__) {
ReactCurrentOwner.current = workInProgress;
- value = fn(props, context);
+ value = fn(nextProps, nextContext);
} else {
- value = fn(props, context);
+ value = fn(nextProps, nextContext);
}
if (
@@ -552,20 +822,39 @@ module.exports = function(
value !== null &&
typeof value.render === 'function'
) {
- // Proceed under the assumption that this is a class instance
+ // Proceed under the assumption that this is a class instance.
workInProgress.tag = ClassComponent;
+ const instance = (workInProgress.stateNode = value);
+ const initialState = instance.state;
+ instance.updater = classUpdater;
+ instance.context = nextContext;
+ ReactInstanceMap.set(instance, workInProgress);
// 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.
- // We will invalidate the child context in finishClassComponent() right after rendering.
- const hasContext = pushContextProvider(workInProgress);
- adoptClassInstance(workInProgress, value);
- mountClassInstance(workInProgress, priorityLevel);
- return finishClassComponent(current, workInProgress, true, hasContext);
+ // During mounting we don't know the child context yet as the instance
+ // doesn't exist. We will invalidate the child context right
+ // after rendering.
+ const hasChildContext = pushContextProvider(workInProgress);
+ unmaskedContext = getUnmaskedContext(workInProgress);
+ nextContext = getMaskedContext(workInProgress, unmaskedContext);
+
+ return beginClassComponentImpl(
+ current,
+ workInProgress,
+ instance,
+ nextProps,
+ nextContext,
+ initialState,
+ hasChildContext,
+ renderPriority,
+ );
} else {
// Proceed under the assumption that this is a functional component
workInProgress.tag = FunctionalComponent;
+ const nextChildren = value;
+
if (__DEV__) {
+ // Mount warnings for functional components
const Component = workInProgress.type;
if (Component) {
@@ -599,268 +888,628 @@ module.exports = function(
}
}
}
- reconcileChildren(current, workInProgress, value);
- memoizeProps(workInProgress, props);
- return workInProgress.child;
+ // Reconcile the children.
+ return reconcile(
+ current,
+ workInProgress,
+ nextChildren,
+ nextProps,
+ null,
+ renderPriority,
+ );
}
}
- function updateCoroutineComponent(current, workInProgress) {
- var nextCoroutine = (workInProgress.pendingProps: null | ReactCoroutine);
- if (hasContextChanged()) {
- // Normally we can bail out on props equality but if context has changed
- // we don't do the bailout and we have to reuse existing props instead.
- if (nextCoroutine === null) {
- nextCoroutine = current && current.memoizedProps;
- invariant(
- nextCoroutine !== null,
- 'We should always have pending or current props. This error is ' +
- 'likely caused by a bug in React. Please file an issue.',
- );
- }
- } else if (
- nextCoroutine === null ||
- workInProgress.memoizedProps === nextCoroutine
+ function beginFunctionalComponent(
+ current: Fiber | null,
+ workInProgress: Fiber,
+ nextProps: any,
+ renderPriority: PriorityLevel,
+ ): Fiber | null {
+ const fn = workInProgress.type;
+
+ const memoizedProps = workInProgress.memoizedProps;
+ if (
+ (nextProps === memoizedProps && !hasContextChanged()) ||
+ // TODO: Disable this before release, since it is not part of the public
+ // API. I use this for testing to compare the relative overhead
+ // of classes.
+ (typeof fn.shouldComponentUpdate === 'function' &&
+ !fn.shouldComponentUpdate(memoizedProps, nextProps))
) {
- nextCoroutine = workInProgress.memoizedProps;
- // TODO: When bailing out, we might need to return the stateNode instead
- // of the child. To check it for work.
- // return bailoutOnAlreadyFinishedWork(current, workInProgress);
+ // No changes to props or context. Bailout.
+ return bailout(current, workInProgress, nextProps, null, renderPriority);
}
- const nextChildren = nextCoroutine.children;
- const priorityLevel = workInProgress.pendingWorkPriority;
+ const unmaskedContext = getUnmaskedContext(workInProgress);
+ const nextContext = getMaskedContext(workInProgress, unmaskedContext);
- // The following is a fork of reconcileChildrenAtPriority but using
- // stateNode to store the child.
+ // Compute the next children.
+ let nextChildren;
+ if (__DEV__) {
+ // In DEV, track the current owner for better stack traces
+ ReactCurrentOwner.current = workInProgress;
+ ReactDebugCurrentFiber.phase = 'render';
+ nextChildren = fn(nextProps, nextContext);
+ ReactDebugCurrentFiber.phase = null;
+ } else {
+ nextChildren = fn(nextProps, nextContext);
+ }
- // At this point any memoization is no longer valid since we'll have changed
- // the children.
- workInProgress.memoizedProps = null;
- if (current === null) {
- workInProgress.stateNode = mountChildFibersInPlace(
- workInProgress,
- workInProgress.stateNode,
- nextChildren,
- priorityLevel,
- );
- } else if (current.child === workInProgress.child) {
- clearDeletions(workInProgress);
+ // Reconcile the children.
+ return reconcile(
+ current,
+ workInProgress,
+ nextChildren,
+ nextProps,
+ null,
+ renderPriority,
+ );
+ }
- workInProgress.stateNode = reconcileChildFibers(
- workInProgress,
- workInProgress.stateNode,
- nextChildren,
- priorityLevel,
- );
+ // ----------------- The Life-Cycle of a Composite Component -----------------
+ // The begin phase (or render phase) of a composite component is when we call
+ // the render method to compute the next set of children. Some lifecycle
+ // methods are also called during this phase. These methods make up the bulk
+ // of a React app's total execution time.
+ //
+ // The begin phase is the part of the React update cycle that is asynchronous
+ // and time-sliced. Ideally, methods in this phase contain no side-effects
+ // (other than scheduling updates with setState, which is fine because the
+ // update queue is managed by React). At the very least, lifecycles in the
+ // begin phase should be resilient to renders that are interrupted, restarted,
+ // or aborted. E.g. componentWillMount may fire twice before its children
+ // are inserted.
+ //
+ // Overview of the composite component begin phase algorithm:
+ // - Do we have new props or context since the last render?
+ // -> componentWillReceiveProps(nextProps, nextContext).
+ // - Process the update queue to compute the next state.
+ // - Do we have new props, context, or state since the last render?
+ // - If they are unchanged -> bailout. Stop working and don't re-render.
+ // - If something did change, we may be able to bailout anyway:
+ // - Is this a forced update (caused by this.forceUpdate())?
+ // -> Can't bailout. Skip subsequent checks and continue rendering.
+ // - Is shouldComponentUpdate defined?
+ // -> shouldComponentUpdate(nextProps, nextState, nextContext)
+ // - If it returns false -> bailout.
+ // - Is this a PureComponent?
+ // -> Shallow compare props and state.
+ // - If they are the same -> bailout.
+ // - Proceed with rendering. Are we mounting a new component, or updating
+ // an existing one?
+ // - Mount -> componentWillMount()
+ // - Update -> componentWillUpdate(nextProps, nextState, nextContext)
+ // - Call render method to compute next children.
+ // - Reconcile next children against the previous set.
+ // - Enter begin phase for children.
+ //
+ // componentDidMount, componentDidUpdate, and componentWillUnount are called
+ // during the commit phase, along with other side-effects like refs,
+ // callbacks, and host mutations (e.g. updating the DOM).
+ // ---------------------------------------------------------------------------
+ function beginClassComponent(
+ current: Fiber | null,
+ workInProgress: Fiber,
+ nextProps: any,
+ renderPriority: PriorityLevel,
+ ): Fiber | null {
+ // 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. We will invalidate the child context right after rendering.
+ const hasChildContext = pushContextProvider(workInProgress);
+
+ const ctor = workInProgress.type;
+
+ const unmaskedContext = getUnmaskedContext(workInProgress);
+ const nextContext = getMaskedContext(workInProgress, unmaskedContext);
+
+ let instance = workInProgress.stateNode;
+ let previousState;
+ if (instance === null) {
+ // This is a fresh component. Construct the public component instance.
+ instance = workInProgress.stateNode = new ctor(nextProps, nextContext);
+ const initialState = (previousState = instance.state);
+ instance.updater = classUpdater;
+ instance.context = nextContext;
+ ReactInstanceMap.set(instance, workInProgress);
+ validateClassInstance(workInProgress, nextProps, initialState);
- transferDeletions(workInProgress);
+ if (
+ ReactFeatureFlags.enableAsyncSubtreeAPI &&
+ ctor.unstable_asyncUpdates === true
+ ) {
+ // This is a special async wrapper component. Enable async scheduling
+ // for this component and all of its children.
+ workInProgress.internalContextTag |= AsyncUpdates;
+ }
} else {
- workInProgress.stateNode = reconcileChildFibersInPlace(
- workInProgress,
- workInProgress.stateNode,
- nextChildren,
- priorityLevel,
- );
-
- transferDeletions(workInProgress);
+ previousState = workInProgress.memoizedState;
}
- memoizeProps(workInProgress, nextCoroutine);
- // This doesn't take arbitrary time so we could synchronously just begin
- // eagerly do the work of workInProgress.child as an optimization.
- return workInProgress.stateNode;
+ return beginClassComponentImpl(
+ current,
+ workInProgress,
+ instance,
+ nextProps,
+ nextContext,
+ previousState,
+ hasChildContext,
+ renderPriority,
+ );
}
- function updatePortalComponent(current, workInProgress) {
- 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
- // we don't do the bailout and we have to reuse existing props instead.
- if (nextChildren === null) {
- nextChildren = current && current.memoizedProps;
- invariant(
- nextChildren != null,
- 'We should always have pending or current props. This error is ' +
- 'likely caused by a bug in React. Please file an issue.',
+ // Split this out so that it can be shared between beginClassComponent and
+ // beginIndeterminateComponent, which have different ways of constructing
+ // the class instance. By the time this method is called, we already have a
+ // class instance.
+ function beginClassComponentImpl(
+ current: Fiber | null,
+ workInProgress: Fiber,
+ instance: any,
+ nextProps: any,
+ nextContext: mixed,
+ // The memoized state, or the initial state for new components
+ previousState: mixed,
+ hasChildContext: boolean,
+ renderPriority: PriorityLevel,
+ ): Fiber | null {
+ const ctor = workInProgress.type;
+ const contextDidChange = hasContextChanged();
+ // TODO: Is there a better way to get the memoized context besides reading
+ // from the instance?
+ const memoizedContext = instance.context;
+ const memoizedProps = workInProgress.memoizedProps;
+ // Don't process the update queue until after componentWillReceiveProps
+ const memoizedState = previousState;
+ let nextState = previousState;
+
+ // Is this the initial render? (Note: different from whether this is initial
+ // mount, since a component may render multiple times before mounting.)
+ const isInitialRender = memoizedProps === null;
+
+ // Check if this is a new component or an update.
+ if (!isInitialRender && (nextProps !== memoizedProps || contextDidChange)) {
+ // This component has been rendered before, and it has received new props
+ // or context since the last render. Call componentWillReceiveProps, if
+ // it exists. This should be called even if the component hasn't mounted
+ // yet (current === null) so that state derived from props stays in sync.
+ const cWRP = instance.componentWillReceiveProps;
+ if (typeof cWRP === 'function') {
+ if (__DEV__) {
+ startPhaseTimer(workInProgress, 'componentWillReceiveProps');
+ }
+ callClassInstanceMethod(
+ instance,
+ cWRP,
+ // this.props, this.context, this.state
+ memoizedProps,
+ memoizedContext,
+ memoizedState,
+ // Arguments
+ nextProps,
+ nextContext,
);
+ if (__DEV__) {
+ stopPhaseTimer();
+ }
+ // Detect direct assignment to this.state.
+ // TODO: Ideally, we should never reference read from the public
+ // instance. It'd be nice to remove support for this eventually.
+ if (instance.state !== memoizedState) {
+ if (__DEV__) {
+ warning(
+ false,
+ '%s.componentWillReceiveProps(): Assigning directly to ' +
+ "this.state is deprecated (except inside a component's " +
+ 'constructor). Use setState instead.',
+ getComponentName(workInProgress),
+ );
+ }
+ classUpdater.enqueueReplaceState(instance, instance.state, null);
+ }
}
- } else if (
- nextChildren === null ||
- workInProgress.memoizedProps === nextChildren
- ) {
- return bailoutOnAlreadyFinishedWork(current, workInProgress);
}
- if (current === null) {
- // Portals are special because we don't append the children during mount
- // but at commit. Therefore we need to track insertions which the normal
- // flow doesn't do during mount. This doesn't happen at the root because
- // the root always starts with a "current" with a null child.
- // TODO: Consider unifying this with how the root works.
- workInProgress.child = reconcileChildFibersInPlace(
+ // Process all the updates in the update queue that satisfy our current
+ // render priority. This will produce a new state object that we can compare
+ // to the memoized state.
+ if (workInProgress.updateQueue !== null) {
+ nextState = beginUpdateQueue(
+ current,
workInProgress,
- workInProgress.child,
- nextChildren,
- priorityLevel,
+ workInProgress.updateQueue,
+ instance,
+ nextState,
+ nextProps,
+ renderPriority,
);
- memoizeProps(workInProgress, nextChildren);
- markChildAsProgressed(current, workInProgress, priorityLevel);
- } else {
- reconcileChildren(current, workInProgress, nextChildren);
- memoizeProps(workInProgress, nextChildren);
}
- return workInProgress.child;
- }
- /*
- function reuseChildrenEffects(returnFiber : Fiber, firstChild : Fiber) {
- let child = firstChild;
- do {
- // Ensure that the first and last effect of the parent corresponds
- // to the children's first and last effect.
- if (!returnFiber.firstEffect) {
- returnFiber.firstEffect = child.firstEffect;
+ // Compare the next inputs (props, context, state) to the memoized inputs
+ // to determine if we should re-render the children or bailout.
+ let shouldUpdate;
+ if (isInitialRender) {
+ shouldUpdate = true;
+ } else if (
+ workInProgress.updateQueue !== null &&
+ workInProgress.updateQueue.hasForceUpdate
+ ) {
+ // This is a forced update. Re-render regardless of shouldComponentUpdate.
+ shouldUpdate = true;
+ } else if (
+ nextProps === memoizedProps &&
+ nextState === memoizedState &&
+ !contextDidChange
+ ) {
+ // None of the inputs have changed. Bailout.
+ shouldUpdate = false;
+ } else if (typeof instance.shouldComponentUpdate === 'function') {
+ // There was a change in props, state, or context. But we may be able to
+ // bailout anyway if shouldComponentUpdate -> false.
+ if (__DEV__) {
+ startPhaseTimer(workInProgress, 'shouldComponentUpdate');
+ }
+ shouldUpdate = callClassInstanceMethod(
+ instance,
+ instance.shouldComponentUpdate,
+ // this.props, this.context, this.state
+ memoizedProps,
+ memoizedContext,
+ memoizedState,
+ // Arguments
+ nextProps,
+ nextState,
+ nextContext,
+ );
+ if (__DEV__) {
+ stopPhaseTimer();
+ warning(
+ shouldUpdate !== undefined,
+ '%s.shouldComponentUpdate(): Returned undefined instead of a ' +
+ 'boolean value. Make sure to return true or false.',
+ getComponentName(workInProgress) || 'Unknown',
+ );
}
- if (child.lastEffect) {
- if (returnFiber.lastEffect) {
- returnFiber.lastEffect.nextEffect = child.firstEffect;
+ } else if (ctor.prototype && ctor.prototype.isPureReactComponent) {
+ // This is a PureComponent. Do a shallow comparison of props and state.
+ shouldUpdate =
+ !shallowEqual(memoizedProps, nextProps) ||
+ !shallowEqual(memoizedState, nextState);
+ } else {
+ // The inputs changed and we can't bail out. Re-render.
+ shouldUpdate = true;
+ }
+
+ if (shouldUpdate) {
+ // If this is not a bailout, call componentWillMount (if this is a mount)
+ // or componentWillUpdate (if this is an update).
+ if (current === null) {
+ // This is a mount. That doesn't mean we haven't rendered this component
+ // before — a previous mount may have been interrupted. Regardless, call
+ // componentWillMount, if it exists.
+ const cWM = instance.componentWillMount;
+ if (typeof cWM === 'function') {
+ if (__DEV__) {
+ startPhaseTimer(workInProgress, 'componentWillMount');
+ }
+ callClassInstanceMethod(
+ instance,
+ cWM,
+ // this.props, this.context, this.state
+ nextProps,
+ nextContext,
+ nextState,
+ // No arguments
+ );
+ if (__DEV__) {
+ stopPhaseTimer();
+ }
+ // Detect direct assignment to this.state.
+ // TODO: Ideally, we should never reference read from the public
+ // instance. It'd be nice to remove support for this eventually.
+ if (instance.state !== nextState) {
+ if (__DEV__) {
+ warning(
+ false,
+ '%s.componentWillMount(): Assigning directly to this.state ' +
+ "is deprecated (except inside a component's constructor). " +
+ 'Use setState instead.',
+ getComponentName(workInProgress),
+ );
+ }
+ classUpdater.enqueueReplaceState(instance, instance.state, null);
+ }
+ }
+ } else {
+ // This is an update. Call componentWillUpdate, if it exists.
+ const cWU = instance.componentWillUpdate;
+ if (typeof cWU === 'function') {
+ if (__DEV__) {
+ startPhaseTimer(workInProgress, 'componentWillUpdate');
+ }
+ callClassInstanceMethod(
+ instance,
+ cWU,
+ // this.props, this.context, this.state
+ memoizedProps,
+ memoizedContext,
+ memoizedState,
+ // Arguments
+ // (The asymmetry between the signatures for componentWillMount and
+ // componentWillUpdate is confusing. Oh well, can't change it now.)
+ nextProps,
+ nextState,
+ nextContext,
+ );
+ if (__DEV__) {
+ stopPhaseTimer();
+ }
+ // Unlike cWRP and cWM, we don't support direct assignment to
+ // this.state inside cWU. We only support it (with a warning) in those
+ // other methods because it happened to work in Stack, and we don't
+ // want to break existing product code.
}
- returnFiber.lastEffect = child.lastEffect;
}
- } while (child = child.sibling);
- }
- */
- function bailoutOnAlreadyFinishedWork(
- current,
- workInProgress: Fiber,
- ): Fiber | null {
- if (__DEV__) {
- cancelWorkTimer(workInProgress);
- }
-
- const priorityLevel = workInProgress.pendingWorkPriority;
- // TODO: We should ideally be able to bail out early if the children have no
- // more work to do. However, since we don't have a separation of this
- // Fiber's priority and its children yet - we don't know without doing lots
- // of the same work we do anyway. Once we have that separation we can just
- // bail out here if the children has no more work at this priority level.
- // if (workInProgress.priorityOfChildren <= priorityLevel) {
- // // If there are side-effects in these children that have not yet been
- // // committed we need to ensure that they get properly transferred up.
- // if (current && current.child !== workInProgress.child) {
- // reuseChildrenEffects(workInProgress, child);
- // }
- // return null;
- // }
-
- if (current && workInProgress.child === current.child) {
- // If we had any progressed work already, that is invalid at this point so
- // let's throw it out.
- clearDeletions(workInProgress);
- }
-
- cloneChildFibers(current, workInProgress);
- markChildAsProgressed(current, workInProgress, priorityLevel);
- return workInProgress.child;
- }
+ // Process the update queue again in case cWM or cWU contained updates.
+ if (workInProgress.updateQueue !== null) {
+ nextState = beginUpdateQueue(
+ current,
+ workInProgress,
+ workInProgress.updateQueue,
+ instance,
+ nextState,
+ nextProps,
+ renderPriority,
+ );
+ }
+ }
+
+ // Determine if any effects need to be scheduled. These should all happen
+ // before bailing out because the effectTag gets reset during reconcilation.
+ // It should also happen after any lifecycles, in case they contain updates.
+
+ // Check if the ref has changed and schedule an effect.
+ checkForUpdatedRef(current, workInProgress);
+
+ // Schedule a callback effect if needed.
+ if (
+ workInProgress.updateQueue !== null &&
+ workInProgress.updateQueue.callbackList !== null
+ ) {
+ workInProgress.effectTag |= Callback;
+ }
+
+ // If we have new props or state since the last commit (includes the
+ // initial mount), we need to schedule an Update effect. This could be
+ // true even if we're about to bailout (shouldUpdate === false), because
+ // a bailout only means that the work-in-progress is up-to-date; it may not
+ // have ever committed. Instead, we need to compare to current to see if
+ // anything changed.
+ if (current === null) {
+ if (typeof instance.componentDidMount === 'function') {
+ workInProgress.effectTag |= Update;
+ }
+ } else if (
+ // TODO: We compare the memoizedProps and memoizedState to current so
+ // that it evaluates to false for a shouldComponentUpdate -> false
+ // bailout. However, this will fail if we bailout with
+ // shouldComponentUpdate twice before committing, because the props and
+ // state are updated after the first bailout. Only observable in async
+ // mode. Need to find a better heuristic. We can't read from the effectTag
+ // because it may have been reset during reconciliation.
+ shouldUpdate ||
+ current.memoizedProps !== workInProgress.memoizedProps ||
+ current.memoizedState !== workInProgress.memoizedState
+ ) {
+ if (typeof instance.componentDidUpdate === 'function') {
+ workInProgress.effectTag |= Update;
+ }
+ }
- function bailoutOnLowPriority(current, workInProgress) {
+ // By now, all effects should have been scheduled. It's safe to bailout.
+ if (!shouldUpdate) {
+ // This is a bailout. Reuse the work without re-rendering.
+ return bailout(
+ current,
+ workInProgress,
+ nextProps,
+ nextState,
+ renderPriority,
+ );
+ }
+
+ // No bailout. Call the render method to get the next set of children.
+ ReactCurrentOwner.current = workInProgress;
+ if (__DEV__) {
+ ReactDebugCurrentFiber.phase = 'render';
+ }
+ const nextChildren = callClassInstanceMethod(
+ instance,
+ instance.render,
+ // this.props, this.context, this.state
+ nextProps,
+ nextContext,
+ nextState,
+ // No arguments
+ );
if (__DEV__) {
- cancelWorkTimer(workInProgress);
+ ReactDebugCurrentFiber.phase = null;
}
- // TODO: Handle HostComponent tags here as well and call pushHostContext()?
- // See PR 8590 discussion for context
- switch (workInProgress.tag) {
- case ClassComponent:
- pushContextProvider(workInProgress);
- break;
- case HostPortal:
- pushHostContainer(
- workInProgress,
- workInProgress.stateNode.containerInfo,
- );
- break;
+ // If this component provides context to its children, we need to
+ // recalcuate it before we start working on them.
+ if (hasChildContext) {
+ invalidateContextProvider(workInProgress);
}
- // TODO: What if this is currently in progress?
- // How can that happen? How is this not being cloned?
- return null;
+
+ // Reconcile the children.
+ return reconcile(
+ current,
+ workInProgress,
+ nextChildren,
+ nextProps,
+ nextState,
+ renderPriority,
+ );
}
- function memoizeProps(workInProgress: Fiber, nextProps: any) {
- workInProgress.memoizedProps = nextProps;
- // Reset the pending props
- workInProgress.pendingProps = null;
+ function beginFragment(
+ current: Fiber | null,
+ workInProgress: Fiber,
+ nextProps: any,
+ renderPriority: PriorityLevel,
+ ): Fiber | null {
+ const memoizedProps = workInProgress.memoizedProps;
+ if (nextProps === memoizedProps && !hasContextChanged()) {
+ // No changes to props or context. Bailout.
+ return bailout(current, workInProgress, nextProps, null, renderPriority);
+ }
+
+ // Compute the next children.
+ const nextChildren = nextProps;
+ return reconcile(
+ current,
+ workInProgress,
+ nextChildren,
+ nextProps,
+ null,
+ renderPriority,
+ );
}
- function memoizeState(workInProgress: Fiber, nextState: any) {
- workInProgress.memoizedState = nextState;
- // Don't reset the updateQueue, in case there are pending updates. Resetting
- // is handled by beginUpdateQueue.
+ function beginCoroutineComponent(
+ current: Fiber | null,
+ workInProgress: Fiber,
+ nextCoroutine: ReactCoroutine,
+ renderPriority: PriorityLevel,
+ ): Fiber | null {
+ // TODO: Bailout if coroutine hasn't changed. When bailing out, we might
+ // need to return the stateNode instead of the child. To check it for work.
+ const child = (workInProgress.stateNode = reconcileImpl(
+ current,
+ workInProgress,
+ workInProgress.stateNode,
+ nextCoroutine.children,
+ nextCoroutine,
+ null,
+ false,
+ renderPriority,
+ ));
+ return child;
}
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
- priorityLevel: PriorityLevel,
+ renderPriority: PriorityLevel,
): Fiber | null {
- if (
- workInProgress.pendingWorkPriority === NoWork ||
- workInProgress.pendingWorkPriority > priorityLevel
- ) {
- return bailoutOnLowPriority(current, workInProgress);
- }
-
if (__DEV__) {
+ // Keep track of the fiber we're currently working on.
ReactDebugCurrentFiber.current = workInProgress;
}
- // If we don't bail out, we're going be recomputing our children so we need
- // to drop our effect list.
+ resumeOrResetWork(current, workInProgress, renderPriority);
+
+ // Clear the effect list, as it's no longer valid.
workInProgress.firstEffect = null;
+ workInProgress.nextEffect = null;
workInProgress.lastEffect = null;
- if (workInProgress.progressedPriority === priorityLevel) {
- // If we have progressed work on this priority level already, we can
- // proceed this that as the child.
- workInProgress.child = workInProgress.progressedChild;
+ let nextProps = workInProgress.pendingProps;
+ if (nextProps === null) {
+ // If there are no pending props, re-use the memoized props.
+ nextProps = workInProgress.memoizedProps;
+ invariant(nextProps !== null, 'Must have pending or memoized props.');
+ } else {
+ // Reset the pending props, since we're about to process them.
+ workInProgress.pendingProps = null;
}
+ let next = null;
switch (workInProgress.tag) {
+ case HostRoot:
+ next = beginHostRoot(
+ current,
+ workInProgress,
+ nextProps,
+ renderPriority,
+ );
+ break;
+ case HostPortal:
+ next = beginHostPortal(
+ current,
+ workInProgress,
+ nextProps,
+ renderPriority,
+ );
+ break;
+ case HostComponent:
+ next = beginHostComponent(
+ current,
+ workInProgress,
+ nextProps,
+ renderPriority,
+ );
+ break;
+ case HostText:
+ next = beginHostText(
+ current,
+ workInProgress,
+ nextProps,
+ renderPriority,
+ );
+ break;
case IndeterminateComponent:
- return mountIndeterminateComponent(
+ next = beginIndeterminateComponent(
current,
workInProgress,
- priorityLevel,
+ nextProps,
+ renderPriority,
);
+ break;
case FunctionalComponent:
- return updateFunctionalComponent(current, workInProgress);
+ next = beginFunctionalComponent(
+ current,
+ workInProgress,
+ nextProps,
+ renderPriority,
+ );
+ break;
case ClassComponent:
- return updateClassComponent(current, workInProgress, priorityLevel);
- case HostRoot:
- return updateHostRoot(current, workInProgress, priorityLevel);
- case HostComponent:
- return updateHostComponent(current, workInProgress);
- case HostText:
- return updateHostText(current, workInProgress);
+ next = beginClassComponent(
+ current,
+ workInProgress,
+ nextProps,
+ renderPriority,
+ );
+ break;
+ case Fragment:
+ next = beginFragment(
+ current,
+ workInProgress,
+ nextProps,
+ renderPriority,
+ );
+ break;
case CoroutineHandlerPhase:
// This is a restart. Reset the tag to the initial phase.
workInProgress.tag = CoroutineComponent;
// Intentionally fall through since this is now the same.
case CoroutineComponent:
- return updateCoroutineComponent(current, workInProgress);
+ next = beginCoroutineComponent(
+ current,
+ workInProgress,
+ nextProps,
+ renderPriority,
+ );
+ break;
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);
- case Fragment:
- return updateFragment(current, workInProgress);
+ next = null;
+ break;
default:
invariant(
false,
@@ -868,12 +1517,23 @@ module.exports = function(
'React. Please file an issue.',
);
}
+
+ // Mark this as the newest work. This is not the same as progressed work,
+ // which only includes re-renders/updates. Keeping track of the lastest
+ // update OR bailout lets us make sure that we don't mistake a "previous
+ // current" fiber for a fresh work-in-progress.
+ workInProgress.newestWork = workInProgress;
+ if (current !== null) {
+ current.newestWork = workInProgress;
+ }
+
+ return next;
}
function beginFailedWork(
current: Fiber | null,
workInProgress: Fiber,
- priorityLevel: PriorityLevel,
+ renderPriority: PriorityLevel,
) {
invariant(
workInProgress.tag === ClassComponent || workInProgress.tag === HostRoot,
@@ -881,33 +1541,40 @@ module.exports = function(
'Please file an issue.',
);
- // Add an error effect so we can handle the error during the commit phase
- workInProgress.effectTag |= Err;
-
- if (
- workInProgress.pendingWorkPriority === NoWork ||
- workInProgress.pendingWorkPriority > priorityLevel
- ) {
- return bailoutOnLowPriority(current, workInProgress);
+ const memoizedProps = workInProgress.memoizedProps;
+ let nextProps = workInProgress.pendingProps;
+ if (nextProps === null) {
+ nextProps = memoizedProps;
}
- // If we don't bail out, we're going be recomputing our children so we need
- // to drop our effect list.
+ // Clear any pending props or updates.
+ workInProgress.pendingProps = null;
+ workInProgress.updateQueue = null;
+
+ // Clear the effect list, as it's no longer valid.
workInProgress.firstEffect = null;
workInProgress.lastEffect = null;
- // Unmount the current children as if the component rendered null
+ // Add an error effect so we can handle the error during the commit phase
+ workInProgress.effectTag |= Err;
+
+ // Unmount the children
const nextChildren = null;
- reconcileChildren(current, workInProgress, nextChildren);
+ const next = reconcile(
+ current,
+ workInProgress,
+ nextChildren,
+ memoizedProps,
+ workInProgress.memoizedState,
+ renderPriority,
+ );
- if (workInProgress.tag === ClassComponent) {
- const instance = workInProgress.stateNode;
- workInProgress.memoizedProps = instance.props;
- workInProgress.memoizedState = instance.state;
- workInProgress.pendingProps = null;
+ workInProgress.newestWork = workInProgress;
+ if (current !== null) {
+ current.newestWork = workInProgress;
}
- return workInProgress.child;
+ return next;
}
return {
@@ -915,3 +1582,4 @@ module.exports = function(
beginFailedWork,
};
};
+exports.BeginWork = BeginWork;
diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js
index 1d695f7cb2e3..9a049e6697ec 100644
--- a/src/renderers/shared/fiber/ReactFiberClassComponent.js
+++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js
@@ -15,35 +15,19 @@
import type {Fiber} from 'ReactFiber';
import type {PriorityLevel} from 'ReactPriorityLevel';
-var {Update} = require('ReactTypeOfSideEffect');
-
-var ReactFeatureFlags = require('ReactFeatureFlags');
-var {AsyncUpdates} = require('ReactTypeOfInternalContext');
-
-var {
- cacheContext,
- getMaskedContext,
- getUnmaskedContext,
- isContextConsumer,
-} = require('ReactFiberContext');
var {
addUpdate,
addReplaceUpdate,
addForceUpdate,
- beginUpdateQueue,
} = require('ReactFiberUpdateQueue');
-var {hasContextChanged} = require('ReactFiberContext');
var {isMounted} = require('ReactFiberTreeReflection');
var ReactInstanceMap = require('ReactInstanceMap');
-var emptyObject = require('fbjs/lib/emptyObject');
var getComponentName = require('getComponentName');
-var shallowEqual = require('fbjs/lib/shallowEqual');
var invariant = require('fbjs/lib/invariant');
const isArray = Array.isArray;
if (__DEV__) {
- var {startPhaseTimer, stopPhaseTimer} = require('ReactDebugFiberPerf');
var warning = require('fbjs/lib/warning');
var warnOnInvalidCallback = function(callback: mixed, callerName: string) {
warning(
@@ -56,16 +40,163 @@ if (__DEV__) {
};
}
-module.exports = function(
+// Call immediately after constructing a class instance.
+function validateClassInstance(
+ workInProgress: Fiber,
+ initialProps: mixed,
+ initialState: mixed,
+) {
+ const instance = workInProgress.stateNode;
+ const ctor = workInProgress.type;
+ if (__DEV__) {
+ const name = getComponentName(workInProgress);
+ const renderPresent = instance.render;
+ warning(
+ renderPresent,
+ '%s(...): No `render` method found on the returned component ' +
+ 'instance: you may have forgotten to define `render`.',
+ name,
+ );
+ const noGetInitialStateOnES6 =
+ !instance.getInitialState ||
+ instance.getInitialState.isReactClassApproved ||
+ initialState;
+ warning(
+ noGetInitialStateOnES6,
+ 'getInitialState was defined on %s, a plain JavaScript class. ' +
+ 'This is only supported for classes created using React.createClass. ' +
+ 'Did you mean to define a state property instead?',
+ name,
+ );
+ const noGetDefaultPropsOnES6 =
+ !instance.getDefaultProps ||
+ instance.getDefaultProps.isReactClassApproved;
+ warning(
+ noGetDefaultPropsOnES6,
+ 'getDefaultProps was defined on %s, a plain JavaScript class. ' +
+ 'This is only supported for classes created using React.createClass. ' +
+ 'Use a static property to define defaultProps instead.',
+ name,
+ );
+ const noInstancePropTypes = !instance.propTypes;
+ warning(
+ noInstancePropTypes,
+ 'propTypes was defined as an instance property on %s. Use a static ' +
+ 'property to define propTypes instead.',
+ name,
+ );
+ const noInstanceContextTypes = !instance.contextTypes;
+ warning(
+ noInstanceContextTypes,
+ 'contextTypes was defined as an instance property on %s. Use a static ' +
+ 'property to define contextTypes instead.',
+ name,
+ );
+ const noComponentShouldUpdate =
+ typeof instance.componentShouldUpdate !== 'function';
+ warning(
+ noComponentShouldUpdate,
+ '%s has a method called ' +
+ 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' +
+ 'The name is phrased as a question because the function is ' +
+ 'expected to return a value.',
+ name,
+ );
+ if (
+ ctor.prototype &&
+ ctor.prototype.isPureReactComponent &&
+ typeof instance.shouldComponentUpdate !== 'undefined'
+ ) {
+ warning(
+ false,
+ '%s has a method called shouldComponentUpdate(). ' +
+ 'shouldComponentUpdate should not be used when extending React.PureComponent. ' +
+ 'Please extend React.Component if shouldComponentUpdate is used.',
+ getComponentName(workInProgress) || 'A pure component',
+ );
+ }
+ const noComponentDidUnmount =
+ typeof instance.componentDidUnmount !== 'function';
+ warning(
+ noComponentDidUnmount,
+ '%s has a method called ' +
+ 'componentDidUnmount(). But there is no such lifecycle method. ' +
+ 'Did you mean componentWillUnmount()?',
+ name,
+ );
+ const noComponentWillRecieveProps =
+ typeof instance.componentWillRecieveProps !== 'function';
+ warning(
+ noComponentWillRecieveProps,
+ '%s has a method called ' +
+ 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?',
+ name,
+ );
+ const hasMutatedProps = instance.props !== initialProps;
+ warning(
+ instance.props === undefined || !hasMutatedProps,
+ '%s(...): When calling super() in `%s`, make sure to pass ' +
+ "up the same props that your component's constructor was passed.",
+ name,
+ name,
+ );
+ const noInstanceDefaultProps = !instance.defaultProps;
+ warning(
+ noInstanceDefaultProps,
+ 'Setting defaultProps as an instance property on %s is not supported and will be ignored.' +
+ ' Instead, define defaultProps as a static property on %s.',
+ name,
+ name,
+ );
+ }
+ if (
+ initialState !== undefined &&
+ (typeof initialState !== 'object' || isArray(initialState))
+ ) {
+ invariant(
+ false,
+ '%s.state: must be set to an object or null',
+ getComponentName(workInProgress),
+ );
+ }
+ if (typeof instance.getChildContext === 'function') {
+ invariant(
+ typeof workInProgress.type.childContextTypes === 'object',
+ '%s.getChildContext(): childContextTypes must be defined in order to ' +
+ 'use getChildContext().',
+ getComponentName(workInProgress),
+ );
+ }
+}
+exports.validateClassInstance = validateClassInstance;
+
+type Callback = () => mixed;
+
+function callClassInstanceMethod(
+ instance: any,
+ lifecycle: (a: A, b: B, c: C) => D,
+ instanceProps: mixed,
+ instanceContext: mixed,
+ instanceState: mixed,
+ a: A,
+ b: B,
+ c: C,
+): D | void {
+ instance.props = instanceProps;
+ instance.context = instanceContext;
+ instance.state = instanceState;
+ const args = Array.prototype.slice.call(arguments, 5);
+ return lifecycle.apply(instance, args);
+}
+exports.callClassInstanceMethod = callClassInstanceMethod;
+
+function ClassUpdater(
scheduleUpdate: (fiber: Fiber, priorityLevel: PriorityLevel) => void,
getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel,
- memoizeProps: (workInProgress: Fiber, props: any) => void,
- memoizeState: (workInProgress: Fiber, state: any) => void,
) {
- // Class component state updater
- const updater = {
+ return {
isMounted,
- enqueueSetState(instance, partialState, callback) {
+ enqueueSetState(instance: any, partialState: mixed, callback: ?Callback) {
const fiber = ReactInstanceMap.get(instance);
const priorityLevel = getPriorityContext(fiber, false);
callback = callback === undefined ? null : callback;
@@ -75,7 +206,7 @@ module.exports = function(
addUpdate(fiber, partialState, callback, priorityLevel);
scheduleUpdate(fiber, priorityLevel);
},
- enqueueReplaceState(instance, state, callback) {
+ enqueueReplaceState(instance: any, state: mixed, callback: ?Callback) {
const fiber = ReactInstanceMap.get(instance);
const priorityLevel = getPriorityContext(fiber, false);
callback = callback === undefined ? null : callback;
@@ -85,7 +216,7 @@ module.exports = function(
addReplaceUpdate(fiber, state, callback, priorityLevel);
scheduleUpdate(fiber, priorityLevel);
},
- enqueueForceUpdate(instance, callback) {
+ enqueueForceUpdate(instance: any, callback: ?Callback) {
const fiber = ReactInstanceMap.get(instance);
const priorityLevel = getPriorityContext(fiber, false);
callback = callback === undefined ? null : callback;
@@ -96,559 +227,5 @@ module.exports = function(
scheduleUpdate(fiber, priorityLevel);
},
};
-
- function checkShouldComponentUpdate(
- workInProgress,
- oldProps,
- newProps,
- oldState,
- newState,
- newContext,
- ) {
- if (
- oldProps === null ||
- (workInProgress.updateQueue !== null &&
- workInProgress.updateQueue.hasForceUpdate)
- ) {
- // If the workInProgress already has an Update effect, return true
- return true;
- }
-
- const instance = workInProgress.stateNode;
- const type = workInProgress.type;
- if (typeof instance.shouldComponentUpdate === 'function') {
- if (__DEV__) {
- startPhaseTimer(workInProgress, 'shouldComponentUpdate');
- }
- const shouldUpdate = instance.shouldComponentUpdate(
- newProps,
- newState,
- newContext,
- );
- if (__DEV__) {
- stopPhaseTimer();
- }
-
- if (__DEV__) {
- warning(
- shouldUpdate !== undefined,
- '%s.shouldComponentUpdate(): Returned undefined instead of a ' +
- 'boolean value. Make sure to return true or false.',
- getComponentName(workInProgress) || 'Unknown',
- );
- }
-
- return shouldUpdate;
- }
-
- if (type.prototype && type.prototype.isPureReactComponent) {
- return (
- !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
- );
- }
-
- return true;
- }
-
- function checkClassInstance(workInProgress: Fiber) {
- const instance = workInProgress.stateNode;
- const type = workInProgress.type;
- if (__DEV__) {
- const name = getComponentName(workInProgress);
- const renderPresent = instance.render;
- warning(
- renderPresent,
- '%s(...): No `render` method found on the returned component ' +
- 'instance: you may have forgotten to define `render`.',
- name,
- );
- const noGetInitialStateOnES6 =
- !instance.getInitialState ||
- instance.getInitialState.isReactClassApproved ||
- instance.state;
- warning(
- noGetInitialStateOnES6,
- 'getInitialState was defined on %s, a plain JavaScript class. ' +
- 'This is only supported for classes created using React.createClass. ' +
- 'Did you mean to define a state property instead?',
- name,
- );
- const noGetDefaultPropsOnES6 =
- !instance.getDefaultProps ||
- instance.getDefaultProps.isReactClassApproved;
- warning(
- noGetDefaultPropsOnES6,
- 'getDefaultProps was defined on %s, a plain JavaScript class. ' +
- 'This is only supported for classes created using React.createClass. ' +
- 'Use a static property to define defaultProps instead.',
- name,
- );
- const noInstancePropTypes = !instance.propTypes;
- warning(
- noInstancePropTypes,
- 'propTypes was defined as an instance property on %s. Use a static ' +
- 'property to define propTypes instead.',
- name,
- );
- const noInstanceContextTypes = !instance.contextTypes;
- warning(
- noInstanceContextTypes,
- 'contextTypes was defined as an instance property on %s. Use a static ' +
- 'property to define contextTypes instead.',
- name,
- );
- const noComponentShouldUpdate =
- typeof instance.componentShouldUpdate !== 'function';
- warning(
- noComponentShouldUpdate,
- '%s has a method called ' +
- 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' +
- 'The name is phrased as a question because the function is ' +
- 'expected to return a value.',
- name,
- );
- if (
- type.prototype &&
- type.prototype.isPureReactComponent &&
- typeof instance.shouldComponentUpdate !== 'undefined'
- ) {
- warning(
- false,
- '%s has a method called shouldComponentUpdate(). ' +
- 'shouldComponentUpdate should not be used when extending React.PureComponent. ' +
- 'Please extend React.Component if shouldComponentUpdate is used.',
- getComponentName(workInProgress) || 'A pure component',
- );
- }
- const noComponentDidUnmount =
- typeof instance.componentDidUnmount !== 'function';
- warning(
- noComponentDidUnmount,
- '%s has a method called ' +
- 'componentDidUnmount(). But there is no such lifecycle method. ' +
- 'Did you mean componentWillUnmount()?',
- name,
- );
- const noComponentWillRecieveProps =
- typeof instance.componentWillRecieveProps !== 'function';
- warning(
- noComponentWillRecieveProps,
- '%s has a method called ' +
- 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?',
- name,
- );
- const hasMutatedProps = instance.props !== workInProgress.pendingProps;
- warning(
- instance.props === undefined || !hasMutatedProps,
- '%s(...): When calling super() in `%s`, make sure to pass ' +
- "up the same props that your component's constructor was passed.",
- name,
- name,
- );
- const noInstanceDefaultProps = !instance.defaultProps;
- warning(
- noInstanceDefaultProps,
- 'Setting defaultProps as an instance property on %s is not supported and will be ignored.' +
- ' Instead, define defaultProps as a static property on %s.',
- name,
- name,
- );
- }
-
- const state = instance.state;
- if (state && (typeof state !== 'object' || isArray(state))) {
- invariant(
- false,
- '%s.state: must be set to an object or null',
- getComponentName(workInProgress),
- );
- }
- if (typeof instance.getChildContext === 'function') {
- invariant(
- typeof workInProgress.type.childContextTypes === 'object',
- '%s.getChildContext(): childContextTypes must be defined in order to ' +
- 'use getChildContext().',
- getComponentName(workInProgress),
- );
- }
- }
-
- function resetInputPointers(workInProgress: Fiber, instance: any) {
- instance.props = workInProgress.memoizedProps;
- instance.state = workInProgress.memoizedState;
- }
-
- function adoptClassInstance(workInProgress: Fiber, instance: any): void {
- instance.updater = updater;
- workInProgress.stateNode = instance;
- // The instance needs access to the fiber so that it can schedule updates
- ReactInstanceMap.set(instance, workInProgress);
- }
-
- function constructClassInstance(workInProgress: Fiber, props: any): any {
- const ctor = workInProgress.type;
- const unmaskedContext = getUnmaskedContext(workInProgress);
- const needsContext = isContextConsumer(workInProgress);
- const context = needsContext
- ? getMaskedContext(workInProgress, unmaskedContext)
- : emptyObject;
- const instance = new ctor(props, context);
- adoptClassInstance(workInProgress, instance);
-
- // Cache unmasked context so we can avoid recreating masked context unless necessary.
- // ReactFiberContext usually updates this cache but can't for newly-created instances.
- if (needsContext) {
- cacheContext(workInProgress, unmaskedContext, context);
- }
-
- return instance;
- }
-
- function callComponentWillMount(workInProgress, instance) {
- if (__DEV__) {
- startPhaseTimer(workInProgress, 'componentWillMount');
- }
- const oldState = instance.state;
- instance.componentWillMount();
- if (__DEV__) {
- stopPhaseTimer();
- }
-
- if (oldState !== instance.state) {
- if (__DEV__) {
- warning(
- false,
- '%s.componentWillMount(): Assigning directly to this.state is ' +
- "deprecated (except inside a component's " +
- 'constructor). Use setState instead.',
- getComponentName(workInProgress),
- );
- }
- updater.enqueueReplaceState(instance, instance.state, null);
- }
- }
-
- function callComponentWillReceiveProps(
- workInProgress,
- instance,
- newProps,
- newContext,
- ) {
- if (__DEV__) {
- startPhaseTimer(workInProgress, 'componentWillReceiveProps');
- }
- const oldState = instance.state;
- instance.componentWillReceiveProps(newProps, newContext);
- if (__DEV__) {
- stopPhaseTimer();
- }
-
- if (instance.state !== oldState) {
- if (__DEV__) {
- warning(
- false,
- '%s.componentWillReceiveProps(): Assigning directly to ' +
- "this.state is deprecated (except inside a component's " +
- 'constructor). Use setState instead.',
- getComponentName(workInProgress),
- );
- }
- updater.enqueueReplaceState(instance, instance.state, null);
- }
- }
-
- // Invokes the mount life-cycles on a previously never rendered instance.
- function mountClassInstance(
- workInProgress: Fiber,
- priorityLevel: PriorityLevel,
- ): void {
- if (__DEV__) {
- checkClassInstance(workInProgress);
- }
-
- const instance = workInProgress.stateNode;
- const state = instance.state || null;
-
- let props = workInProgress.pendingProps;
- invariant(
- props,
- 'There must be pending props for an initial mount. This error is ' +
- 'likely caused by a bug in React. Please file an issue.',
- );
-
- const unmaskedContext = getUnmaskedContext(workInProgress);
-
- instance.props = props;
- instance.state = state;
- instance.refs = emptyObject;
- instance.context = getMaskedContext(workInProgress, unmaskedContext);
-
- if (
- ReactFeatureFlags.enableAsyncSubtreeAPI &&
- workInProgress.type != null &&
- workInProgress.type.unstable_asyncUpdates === true
- ) {
- workInProgress.internalContextTag |= AsyncUpdates;
- }
-
- if (typeof instance.componentWillMount === 'function') {
- callComponentWillMount(workInProgress, instance);
- // If we had additional state updates during this life-cycle, let's
- // process them now.
- const updateQueue = workInProgress.updateQueue;
- if (updateQueue !== null) {
- instance.state = beginUpdateQueue(
- workInProgress,
- updateQueue,
- instance,
- state,
- props,
- priorityLevel,
- );
- }
- }
- if (typeof instance.componentDidMount === 'function') {
- workInProgress.effectTag |= Update;
- }
- }
-
- // Called on a preexisting class instance. Returns false if a resumed render
- // could be reused.
- function resumeMountClassInstance(
- workInProgress: Fiber,
- priorityLevel: PriorityLevel,
- ): boolean {
- const instance = workInProgress.stateNode;
- resetInputPointers(workInProgress, instance);
-
- let newState = workInProgress.memoizedState;
- let newProps = workInProgress.pendingProps;
- if (!newProps) {
- // If there isn't any new props, then we'll reuse the memoized props.
- // This could be from already completed work.
- newProps = workInProgress.memoizedProps;
- invariant(
- newProps != null,
- 'There should always be pending or memoized props. This error is ' +
- 'likely caused by a bug in React. Please file an issue.',
- );
- }
- const newUnmaskedContext = getUnmaskedContext(workInProgress);
- const newContext = getMaskedContext(workInProgress, newUnmaskedContext);
-
- const oldContext = instance.context;
- const oldProps = workInProgress.memoizedProps;
-
- if (
- typeof instance.componentWillReceiveProps === 'function' &&
- (oldProps !== newProps || oldContext !== newContext)
- ) {
- callComponentWillReceiveProps(
- workInProgress,
- instance,
- newProps,
- newContext,
- );
- }
-
- // Process the update queue before calling shouldComponentUpdate
- const updateQueue = workInProgress.updateQueue;
- if (updateQueue !== null) {
- newState = beginUpdateQueue(
- workInProgress,
- updateQueue,
- instance,
- newState,
- newProps,
- priorityLevel,
- );
- }
-
- // TODO: Should we deal with a setState that happened after the last
- // componentWillMount and before this componentWillMount? Probably
- // unsupported anyway.
-
- if (
- !checkShouldComponentUpdate(
- workInProgress,
- workInProgress.memoizedProps,
- newProps,
- workInProgress.memoizedState,
- newState,
- newContext,
- )
- ) {
- // Update the existing instance's state, props, and context pointers even
- // though we're bailing out.
- instance.props = newProps;
- instance.state = newState;
- instance.context = newContext;
- return false;
- }
-
- // Update the input pointers now so that they are correct when we call
- // componentWillMount
- instance.props = newProps;
- instance.state = newState;
- instance.context = newContext;
-
- if (typeof instance.componentWillMount === 'function') {
- callComponentWillMount(workInProgress, instance);
- // componentWillMount may have called setState. Process the update queue.
- const newUpdateQueue = workInProgress.updateQueue;
- if (newUpdateQueue !== null) {
- newState = beginUpdateQueue(
- workInProgress,
- newUpdateQueue,
- instance,
- newState,
- newProps,
- priorityLevel,
- );
- }
- }
-
- if (typeof instance.componentDidMount === 'function') {
- workInProgress.effectTag |= Update;
- }
-
- instance.state = newState;
-
- return true;
- }
-
- // Invokes the update life-cycles and returns false if it shouldn't rerender.
- function updateClassInstance(
- current: Fiber,
- workInProgress: Fiber,
- priorityLevel: PriorityLevel,
- ): boolean {
- const instance = workInProgress.stateNode;
- resetInputPointers(workInProgress, instance);
-
- const oldProps = workInProgress.memoizedProps;
- let newProps = workInProgress.pendingProps;
- if (!newProps) {
- // If there aren't any new props, then we'll reuse the memoized props.
- // This could be from already completed work.
- newProps = oldProps;
- invariant(
- newProps != null,
- 'There should always be pending or memoized props. This error is ' +
- 'likely caused by a bug in React. Please file an issue.',
- );
- }
- const oldContext = instance.context;
- const newUnmaskedContext = getUnmaskedContext(workInProgress);
- const newContext = getMaskedContext(workInProgress, newUnmaskedContext);
-
- // Note: During these life-cycles, instance.props/instance.state are what
- // ever the previously attempted to render - not the "current". However,
- // during componentDidUpdate we pass the "current" props.
-
- if (
- typeof instance.componentWillReceiveProps === 'function' &&
- (oldProps !== newProps || oldContext !== newContext)
- ) {
- callComponentWillReceiveProps(
- workInProgress,
- instance,
- newProps,
- newContext,
- );
- }
-
- // Compute the next state using the memoized state and the update queue.
- const updateQueue = workInProgress.updateQueue;
- const oldState = workInProgress.memoizedState;
- // TODO: Previous state can be null.
- let newState;
- if (updateQueue !== null) {
- newState = beginUpdateQueue(
- workInProgress,
- updateQueue,
- instance,
- oldState,
- newProps,
- priorityLevel,
- );
- } else {
- newState = oldState;
- }
-
- if (
- oldProps === newProps &&
- oldState === newState &&
- !hasContextChanged() &&
- !(updateQueue !== null && updateQueue.hasForceUpdate)
- ) {
- // If an update was already in progress, we should schedule an Update
- // effect even though we're bailing out, so that cWU/cDU are called.
- if (typeof instance.componentDidUpdate === 'function') {
- if (
- oldProps !== current.memoizedProps ||
- oldState !== current.memoizedState
- ) {
- workInProgress.effectTag |= Update;
- }
- }
- return false;
- }
-
- const shouldUpdate = checkShouldComponentUpdate(
- workInProgress,
- oldProps,
- newProps,
- oldState,
- newState,
- newContext,
- );
-
- if (shouldUpdate) {
- if (typeof instance.componentWillUpdate === 'function') {
- if (__DEV__) {
- startPhaseTimer(workInProgress, 'componentWillUpdate');
- }
- instance.componentWillUpdate(newProps, newState, newContext);
- if (__DEV__) {
- stopPhaseTimer();
- }
- }
- if (typeof instance.componentDidUpdate === 'function') {
- workInProgress.effectTag |= Update;
- }
- } else {
- // If an update was already in progress, we should schedule an Update
- // effect even though we're bailing out, so that cWU/cDU are called.
- if (typeof instance.componentDidUpdate === 'function') {
- if (
- oldProps !== current.memoizedProps ||
- oldState !== current.memoizedState
- ) {
- workInProgress.effectTag |= Update;
- }
- }
-
- // If shouldComponentUpdate returned false, we should still update the
- // memoized props/state to indicate that this work can be reused.
- memoizeProps(workInProgress, newProps);
- memoizeState(workInProgress, newState);
- }
-
- // Update the existing instance's state, props, and context pointers even
- // if shouldComponentUpdate returns false.
- instance.props = newProps;
- instance.state = newState;
- instance.context = newContext;
-
- return shouldUpdate;
- }
-
- return {
- adoptClassInstance,
- constructClassInstance,
- mountClassInstance,
- resumeMountClassInstance,
- updateClassInstance,
- };
-};
+}
+exports.ClassUpdater = ClassUpdater;
diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js
index 7fe0efad7233..8d5f79c19a59 100644
--- a/src/renderers/shared/fiber/ReactFiberCommitWork.js
+++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js
@@ -24,6 +24,7 @@ var {
HostPortal,
CoroutineComponent,
} = ReactTypeOfWork;
+var {callClassInstanceMethod} = require('ReactFiberClassComponent');
var {commitCallbacks} = require('ReactFiberUpdateQueue');
var {onCommitUnmount} = require('ReactFiberDevToolsHook');
var {invokeGuardedCallback} = require('ReactErrorUtils');
@@ -59,7 +60,14 @@ module.exports = function(
if (__DEV__) {
var callComponentWillUnmountWithTimerInDev = function(current, instance) {
startPhaseTimer(current, 'componentWillUnmount');
- instance.componentWillUnmount();
+ callClassInstanceMethod(
+ instance,
+ instance.componentWillUnmount,
+ current.memoizedProps,
+ // TODO: Is there a better way to get the memoized context?
+ instance.context,
+ current.memoizedState,
+ );
stopPhaseTimer();
};
}
@@ -79,7 +87,14 @@ module.exports = function(
}
} else {
try {
- instance.componentWillUnmount();
+ callClassInstanceMethod(
+ instance,
+ instance.componentWillUnmount,
+ current.memoizedProps,
+ // TODO: Is there a better way to get the memoized context?
+ instance.context,
+ current.memoizedState,
+ );
} catch (unmountError) {
captureError(current, unmountError);
}
@@ -450,17 +465,32 @@ module.exports = function(
if (__DEV__) {
startPhaseTimer(finishedWork, 'componentDidMount');
}
- instance.componentDidMount();
+ callClassInstanceMethod(
+ instance,
+ instance.componentDidMount,
+ instance.props, // finishedWork.memoizedProps,
+ // TODO: Is there a better way to get the memoized context?
+ instance.context,
+ instance.state, // finishedWork.memoizedState,
+ );
if (__DEV__) {
stopPhaseTimer();
}
} else {
- const prevProps = current.memoizedProps;
- const prevState = current.memoizedState;
if (__DEV__) {
startPhaseTimer(finishedWork, 'componentDidUpdate');
}
- instance.componentDidUpdate(prevProps, prevState);
+ callClassInstanceMethod(
+ instance,
+ instance.componentDidUpdate,
+ finishedWork.memoizedProps,
+ // TODO: Is there a better way to get the memoized context?
+ instance.context,
+ finishedWork.memoizedState,
+ // Arguments
+ current.memoizedProps,
+ current.memoizedState,
+ );
if (__DEV__) {
stopPhaseTimer();
}
diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js
index 9e95547b48f0..8dca8760e649 100644
--- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js
+++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js
@@ -18,11 +18,15 @@ import type {HostContext} from 'ReactFiberHostContext';
import type {HydrationContext} from 'ReactFiberHydrationContext';
import type {FiberRoot} from 'ReactFiberRoot';
import type {HostConfig} from 'ReactFiberReconciler';
+import type {PriorityLevel} from 'ReactPriorityLevel';
-var {reconcileChildFibers} = require('ReactChildFiber');
+var {
+ transferEffectsToParent,
+ getPriorityFromChildren,
+ largerPriority,
+} = require('ReactFiber');
+var {reconcile} = require('ReactFiberBeginWork');
var {popContextProvider} = require('ReactFiberContext');
-var ReactTypeOfWork = require('ReactTypeOfWork');
-var ReactTypeOfSideEffect = require('ReactTypeOfSideEffect');
var {
IndeterminateComponent,
FunctionalComponent,
@@ -35,8 +39,9 @@ var {
CoroutineHandlerPhase,
YieldComponent,
Fragment,
-} = ReactTypeOfWork;
-var {Placement, Ref, Update} = ReactTypeOfSideEffect;
+} = require('ReactTypeOfWork');
+var {Placement, Update} = require('ReactTypeOfSideEffect');
+var {NoWork, OffscreenPriority} = require('ReactPriorityLevel');
if (__DEV__) {
var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber');
@@ -44,7 +49,7 @@ if (__DEV__) {
var invariant = require('fbjs/lib/invariant');
-module.exports = function(
+exports.CompleteWork = function(
config: HostConfig,
hostContext: HostContext,
hydrationContext: HydrationContext,
@@ -70,28 +75,6 @@ module.exports = function(
popHydrationState,
} = hydrationContext;
- function markChildAsProgressed(current, workInProgress, priorityLevel) {
- // We now have clones. Let's store them as the currently progressed work.
- workInProgress.progressedChild = workInProgress.child;
- workInProgress.progressedPriority = priorityLevel;
- if (current !== null) {
- // We also store it on the current. When the alternate swaps in we can
- // continue from this point.
- current.progressedChild = workInProgress.progressedChild;
- current.progressedPriority = workInProgress.progressedPriority;
- }
- }
-
- function markUpdate(workInProgress: Fiber) {
- // Tag the fiber with an update effect. This turns a Placement into
- // an UpdateAndPlacement.
- workInProgress.effectTag |= Update;
- }
-
- function markRef(workInProgress: Fiber) {
- workInProgress.effectTag |= Ref;
- }
-
function appendAllYields(yields: Array, workInProgress: Fiber) {
let node = workInProgress.stateNode;
if (node) {
@@ -125,8 +108,9 @@ module.exports = function(
function moveCoroutineToHandlerPhase(
current: Fiber | null,
workInProgress: Fiber,
- ) {
- var coroutine = (workInProgress.memoizedProps: ?ReactCoroutine);
+ renderPriority: PriorityLevel,
+ ): Fiber | null {
+ const coroutine = (workInProgress.memoizedProps: ?ReactCoroutine);
invariant(
coroutine,
'Should be resolved by now. This error is likely caused by a bug in ' +
@@ -144,23 +128,20 @@ module.exports = function(
// Build up the yields.
// TODO: Compare this to a generator or opaque helpers like Children.
- var yields: Array = [];
+ const yields: Array = [];
appendAllYields(yields, workInProgress);
- var fn = coroutine.handler;
- var props = coroutine.props;
- var nextChildren = fn(props, yields);
+ const fn = coroutine.handler;
+ const props = coroutine.props;
+ const nextChildren = fn(props, yields);
- var currentFirstChild = current !== null ? current.child : null;
- // Inherit the priority of the returnFiber.
- const priority = workInProgress.pendingWorkPriority;
- workInProgress.child = reconcileChildFibers(
+ return reconcile(
+ current,
workInProgress,
- currentFirstChild,
nextChildren,
- priority,
+ coroutine,
+ null,
+ renderPriority,
);
- markChildAsProgressed(current, workInProgress, priority);
- return workInProgress.child;
}
function appendAllChildren(parent: I, workInProgress: Fiber) {
@@ -194,18 +175,21 @@ module.exports = function(
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
+ renderPriority: PriorityLevel,
): Fiber | null {
if (__DEV__) {
ReactDebugCurrentFiber.current = workInProgress;
}
+ let next = null;
+
switch (workInProgress.tag) {
case FunctionalComponent:
- return null;
+ break;
case ClassComponent: {
// We are leaving this subtree, so pop context if any.
popContextProvider(workInProgress);
- return null;
+ break;
}
case HostRoot: {
// TODO: Pop the host container after #8607 lands.
@@ -214,7 +198,6 @@ module.exports = function(
fiberRoot.context = fiberRoot.pendingContext;
fiberRoot.pendingContext = null;
}
-
if (current === null || current.child === null) {
// If we hydrated, pop so that we can delete any remaining children
// that weren't hydrated.
@@ -223,7 +206,7 @@ module.exports = function(
// TODO: Delete this when we delete isMounted and findDOMNode.
workInProgress.effectTag &= ~Placement;
}
- return null;
+ break;
}
case HostComponent: {
popHostContext(workInProgress);
@@ -254,20 +237,17 @@ module.exports = function(
// If the update payload indicates that there is a change or if there
// is a new ref we mark this as an update.
if (updatePayload) {
- markUpdate(workInProgress);
- }
- if (current.ref !== workInProgress.ref) {
- markRef(workInProgress);
+ workInProgress.effectTag |= Update;
}
} else {
- if (!newProps) {
+ if (newProps === null) {
invariant(
workInProgress.stateNode !== null,
'We must have new props for new mounts. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
// This can happen when we abort work.
- return null;
+ break;
}
const currentHostContext = getHostContext();
@@ -304,17 +284,13 @@ module.exports = function(
rootContainerInstance,
)
) {
- markUpdate(workInProgress);
+ workInProgress.effectTag |= Update;
}
}
workInProgress.stateNode = instance;
- if (workInProgress.ref !== null) {
- // If there is a ref on a host node we need to schedule a callback
- markRef(workInProgress);
- }
}
- return null;
+ break;
}
case HostText: {
let newText = workInProgress.memoizedProps;
@@ -323,7 +299,7 @@ module.exports = function(
// If we have an alternate, that means this is an update and we need
// to schedule a side-effect to do the updates.
if (oldText !== newText) {
- markUpdate(workInProgress);
+ workInProgress.effectTag |= Update;
}
} else {
if (typeof newText !== 'string') {
@@ -333,7 +309,7 @@ module.exports = function(
'caused by a bug in React. Please file an issue.',
);
// This can happen when we abort work.
- return null;
+ break;
}
const rootContainerInstance = getRootHostContainer();
const currentHostContext = getHostContext();
@@ -351,24 +327,29 @@ module.exports = function(
}
workInProgress.stateNode = textInstance;
}
- return null;
+ break;
}
case CoroutineComponent:
- return moveCoroutineToHandlerPhase(current, workInProgress);
+ next = moveCoroutineToHandlerPhase(
+ current,
+ workInProgress,
+ renderPriority,
+ );
+ break;
case CoroutineHandlerPhase:
// Reset the tag to now be a first phase coroutine.
workInProgress.tag = CoroutineComponent;
- return null;
+ break;
case YieldComponent:
// Does nothing.
- return null;
+ break;
case Fragment:
- return null;
+ break;
case HostPortal:
// TODO: Only mark this as an update if we have any pending callbacks.
- markUpdate(workInProgress);
+ workInProgress.effectTag |= Update;
popHostContainer(workInProgress);
- return null;
+ break;
// Error cases
case IndeterminateComponent:
invariant(
@@ -385,6 +366,36 @@ module.exports = function(
'React. Please file an issue.',
);
}
+
+ // Work in this tree was just completed. There may be lower priority
+ // remaining. Reset the work priority by bubbling it up from the children.
+ // We do this regardless of whether the child is current or
+ // a work-in-progress, because the current children may have pending work
+ // that's not in the work-in-progress children.
+ let remainingWorkPriority = NoWork;
+
+ const progressedWork = workInProgress.progressedWork;
+ if (progressedWork !== current && progressedWork !== workInProgress) {
+ remainingWorkPriority = workInProgress.progressedPriority;
+ }
+
+ // Bubble up priority from the children, unless the children are offscreen,
+ // in which case work should be deprioritized.
+ // TODO: How will this work with expiration times?
+ if (remainingWorkPriority !== OffscreenPriority) {
+ remainingWorkPriority = largerPriority(
+ remainingWorkPriority,
+ getPriorityFromChildren(workInProgress),
+ );
+ }
+ workInProgress.pendingWorkPriority = remainingWorkPriority;
+
+ // Transfer effects list to the parent
+ if (workInProgress.return !== null) {
+ transferEffectsToParent(workInProgress.return, workInProgress);
+ }
+
+ return next;
}
return {
diff --git a/src/renderers/shared/fiber/ReactFiberHydrationContext.js b/src/renderers/shared/fiber/ReactFiberHydrationContext.js
index e3043258dc85..8b0048e3c903 100644
--- a/src/renderers/shared/fiber/ReactFiberHydrationContext.js
+++ b/src/renderers/shared/fiber/ReactFiberHydrationContext.js
@@ -90,21 +90,15 @@ module.exports = function(
childToDelete.stateNode = instance;
childToDelete.return = returnFiber;
// Deletions are added in reversed order so we add it to the front.
- const last = returnFiber.progressedLastDeletion;
+ const last = returnFiber.lastDeletion;
if (last !== null) {
last.nextEffect = childToDelete;
- returnFiber.progressedLastDeletion = childToDelete;
+ returnFiber.lastDeletion = childToDelete;
} else {
- returnFiber.progressedFirstDeletion = returnFiber.progressedLastDeletion = childToDelete;
+ returnFiber.firstDeletion = returnFiber.lastDeletion = childToDelete;
}
+ childToDelete.nextEffect = null;
childToDelete.effectTag = Deletion;
-
- if (returnFiber.lastEffect !== null) {
- returnFiber.lastEffect.nextEffect = childToDelete;
- returnFiber.lastEffect = childToDelete;
- } else {
- returnFiber.firstEffect = returnFiber.lastEffect = childToDelete;
- }
}
function tryToClaimNextHydratableInstance(fiber: Fiber) {
diff --git a/src/renderers/shared/fiber/ReactFiberRoot.js b/src/renderers/shared/fiber/ReactFiberRoot.js
index 409b70655445..e2f3c19aadc6 100644
--- a/src/renderers/shared/fiber/ReactFiberRoot.js
+++ b/src/renderers/shared/fiber/ReactFiberRoot.js
@@ -13,8 +13,10 @@
'use strict';
import type {Fiber} from 'ReactFiber';
+import type {PriorityLevel} from 'ReactPriorityLevel';
const {createHostRootFiber} = require('ReactFiber');
+const {NoWork} = require('ReactPriorityLevel');
export type FiberRoot = {
// Any additional information from the host associated with this root.
@@ -25,6 +27,10 @@ export type FiberRoot = {
isScheduled: boolean,
// The work schedule is a linked list.
nextScheduledRoot: FiberRoot | null,
+ pendingWorkPriority: PriorityLevel,
+ // The effect list, used during the commit phase.
+ firstEffect: Fiber | null,
+ lastEffect: Fiber | null,
// Top context object, used by renderSubtreeIntoContainer
context: Object | null,
pendingContext: Object | null,
@@ -39,6 +45,9 @@ exports.createFiberRoot = function(containerInfo: any): FiberRoot {
containerInfo: containerInfo,
isScheduled: false,
nextScheduledRoot: null,
+ pendingWorkPriority: NoWork,
+ firstEffect: null,
+ lastEffect: null,
context: null,
pendingContext: null,
};
diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js
index e010390bc8dd..29ed2afbe87b 100644
--- a/src/renderers/shared/fiber/ReactFiberScheduler.js
+++ b/src/renderers/shared/fiber/ReactFiberScheduler.js
@@ -40,15 +40,19 @@ var {
var {logCapturedError} = require('ReactFiberErrorLogger');
var {invokeGuardedCallback} = require('ReactErrorUtils');
-var ReactFiberBeginWork = require('ReactFiberBeginWork');
-var ReactFiberCompleteWork = require('ReactFiberCompleteWork');
+var {BeginWork} = require('ReactFiberBeginWork');
+var {CompleteWork} = require('ReactFiberCompleteWork');
var ReactFiberCommitWork = require('ReactFiberCommitWork');
var ReactFiberHostContext = require('ReactFiberHostContext');
var ReactFiberHydrationContext = require('ReactFiberHydrationContext');
var {ReactCurrentOwner} = require('ReactGlobalSharedState');
var getComponentName = require('getComponentName');
-var {cloneFiber} = require('ReactFiber');
+var {
+ createWorkInProgress,
+ largerPriority,
+ transferEffectsToParent,
+} = require('ReactFiber');
var {onCommitRoot} = require('ReactFiberDevToolsHook');
var {
@@ -64,7 +68,6 @@ var {
var {AsyncUpdates} = require('ReactTypeOfInternalContext');
var {
- NoEffect,
Placement,
Update,
PlacementAndUpdate,
@@ -82,11 +85,12 @@ var {
ClassComponent,
} = require('ReactTypeOfWork');
-var {getPendingPriority} = require('ReactFiberUpdateQueue');
+var {getUpdatePriority} = require('ReactFiberUpdateQueue');
var {resetContext} = require('ReactFiberContext');
var invariant = require('fbjs/lib/invariant');
+var emptyObject = require('fbjs/lib/emptyObject');
if (__DEV__) {
var warning = require('fbjs/lib/warning');
@@ -152,18 +156,14 @@ module.exports = function(
C
> = ReactFiberHydrationContext(config);
const {popHostContainer, popHostContext, resetHostContainer} = hostContext;
- const {beginWork, beginFailedWork} = ReactFiberBeginWork(
+ const {beginWork, beginFailedWork} = BeginWork(
config,
hostContext,
hydrationContext,
scheduleUpdate,
getPriorityContext,
);
- const {completeWork} = ReactFiberCompleteWork(
- config,
- hostContext,
- hydrationContext,
- );
+ const {completeWork} = CompleteWork(config, hostContext, hydrationContext);
const {
commitPlacement,
commitDeletion,
@@ -261,7 +261,7 @@ module.exports = function(
// Clear out roots with no more work on them, or if they have uncaught errors
while (
nextScheduledRoot !== null &&
- nextScheduledRoot.current.pendingWorkPriority === NoWork
+ nextScheduledRoot.pendingWorkPriority === NoWork
) {
// Unschedule this root.
nextScheduledRoot.isScheduled = false;
@@ -280,20 +280,18 @@ module.exports = function(
// If there's no work on it, it will get unscheduled too.
nextScheduledRoot = next;
}
-
let root = nextScheduledRoot;
let highestPriorityRoot = null;
let highestPriorityLevel = NoWork;
while (root !== null) {
+ const rootPriority = root.pendingWorkPriority;
if (
- root.current.pendingWorkPriority !== NoWork &&
- (highestPriorityLevel === NoWork ||
- highestPriorityLevel > root.current.pendingWorkPriority)
+ rootPriority !== NoWork &&
+ (highestPriorityLevel === NoWork || highestPriorityLevel > rootPriority)
) {
- highestPriorityLevel = root.current.pendingWorkPriority;
+ highestPriorityLevel = rootPriority;
highestPriorityRoot = root;
}
- // We didn't find anything to do in this root, so let's try the next one.
root = root.nextScheduledRoot;
}
if (highestPriorityRoot !== null) {
@@ -307,7 +305,7 @@ module.exports = function(
// unfortunately this is it.
resetContextStack();
- return cloneFiber(highestPriorityRoot.current, highestPriorityLevel);
+ return createWorkInProgress(highestPriorityRoot.current, emptyObject);
}
nextPriorityLevel = NoWork;
@@ -446,22 +444,16 @@ module.exports = function(
const previousPriorityContext = priorityContext;
priorityContext = TaskPriority;
- let firstEffect;
- if (finishedWork.effectTag !== NoEffect) {
- // A fiber's effect list consists only of its children, not itself. So if
- // the root has an effect, we need to add it to the end of the list. The
- // resulting list is the set that would belong to the root's parent, if
- // it had one; that is, all the effects in the tree including the root.
- if (finishedWork.lastEffect !== null) {
- finishedWork.lastEffect.nextEffect = finishedWork;
- firstEffect = finishedWork.firstEffect;
- } else {
- firstEffect = finishedWork;
- }
- } else {
- // There is no effect on the root.
- firstEffect = finishedWork.firstEffect;
- }
+ // Update the pending work priority of the root
+ const remainingWorkPriority = largerPriority(
+ finishedWork.pendingWorkPriority,
+ getUpdatePriority(finishedWork),
+ );
+ root.pendingWorkPriority = remainingWorkPriority;
+
+ // Transfer effects from the host root onto the fiber root.
+ transferEffectsToParent(root, finishedWork);
+ const firstEffect = root.firstEffect;
prepareForCommit();
@@ -559,42 +551,11 @@ module.exports = function(
commitPhaseBoundaries = null;
}
- priorityContext = previousPriorityContext;
- }
-
- function resetWorkPriority(workInProgress: Fiber) {
- let newPriority = NoWork;
-
- // Check for pending update priority. This is usually null so it shouldn't
- // be a perf issue.
- const queue = workInProgress.updateQueue;
- const tag = workInProgress.tag;
- if (
- queue !== null &&
- // TODO: Revisit once updateQueue is typed properly to distinguish between
- // update payloads for host components and update queues for composites
- (tag === ClassComponent || tag === HostRoot)
- ) {
- newPriority = getPendingPriority(queue);
- }
-
- // TODO: Coroutines need to visit stateNode
+ // Reset the fiber root's effect list.
+ root.firstEffect = null;
+ root.lastEffect = null;
- // progressedChild is going to be the child set with the highest priority.
- // Either it is the same as child, or it just bailed out because it choose
- // not to do the work.
- let child = workInProgress.progressedChild;
- while (child !== null) {
- // Ensure that remaining work priority bubbles up.
- if (
- child.pendingWorkPriority !== NoWork &&
- (newPriority === NoWork || newPriority > child.pendingWorkPriority)
- ) {
- newPriority = child.pendingWorkPriority;
- }
- child = child.sibling;
- }
- workInProgress.pendingWorkPriority = newPriority;
+ priorityContext = previousPriorityContext;
}
function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
@@ -604,13 +565,11 @@ module.exports = function(
// means that we don't need an additional field on the work in
// progress.
const current = workInProgress.alternate;
- const next = completeWork(current, workInProgress);
+ const next = completeWork(current, workInProgress, nextPriorityLevel);
const returnFiber = workInProgress.return;
const siblingFiber = workInProgress.sibling;
- resetWorkPriority(workInProgress);
-
if (next !== null) {
if (__DEV__) {
stopWorkTimer(workInProgress);
@@ -623,36 +582,6 @@ module.exports = function(
return next;
}
- if (returnFiber !== null) {
- // Append all the effects of the subtree and this fiber onto the effect
- // list of the parent. The completion order of the children affects the
- // side-effect order.
- if (returnFiber.firstEffect === null) {
- returnFiber.firstEffect = workInProgress.firstEffect;
- }
- if (workInProgress.lastEffect !== null) {
- if (returnFiber.lastEffect !== null) {
- returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
- }
- returnFiber.lastEffect = workInProgress.lastEffect;
- }
-
- // If this fiber had side-effects, we append it AFTER the children's
- // side-effects. We can perform certain side-effects earlier if
- // needed, by doing multiple passes over the effect list. We don't want
- // to schedule our own side-effect on our own list because if end up
- // reusing children we'll schedule this effect onto itself since we're
- // at the end.
- if (workInProgress.effectTag !== NoEffect) {
- if (returnFiber.lastEffect !== null) {
- returnFiber.lastEffect.nextEffect = workInProgress;
- } else {
- returnFiber.firstEffect = workInProgress;
- }
- returnFiber.lastEffect = workInProgress;
- }
- }
-
if (__DEV__) {
stopWorkTimer(workInProgress);
}
@@ -843,7 +772,7 @@ module.exports = function(
}
function performWork(
- priorityLevel: PriorityLevel,
+ minPriorityLevel: PriorityLevel,
deadline: Deadline | null,
) {
if (__DEV__) {
@@ -861,9 +790,9 @@ module.exports = function(
// This outer loop exists so that we can restart the work loop after
// catching an error. It also lets us flush Task work at the end of a
// deferred batch.
- while (priorityLevel !== NoWork && !fatalError) {
+ while (minPriorityLevel !== NoWork && fatalError === null) {
invariant(
- deadline !== null || priorityLevel < HighPriority,
+ deadline !== null || minPriorityLevel < HighPriority,
'Cannot perform deferred work without a deadline. This error is ' +
'likely caused by a bug in React. Please file an issue.',
);
@@ -884,12 +813,12 @@ module.exports = function(
null,
workLoop,
null,
- priorityLevel,
+ minPriorityLevel,
deadline,
);
} else {
try {
- workLoop(priorityLevel, deadline);
+ workLoop(minPriorityLevel, deadline);
} catch (e) {
error = e;
}
@@ -911,7 +840,9 @@ module.exports = function(
// Complete the boundary as if it rendered null. This will unmount
// the failed tree.
- beginFailedWork(boundary.alternate, boundary, priorityLevel);
+ // TODO: We should unmount with task priority so that nothing else
+ // can render before it's unmounted.
+ beginFailedWork(boundary.alternate, boundary, nextPriorityLevel);
// The next unit of work is now the boundary that captured the error.
// Conceptually, we're unwinding the stack. We need to unwind the
@@ -935,7 +866,7 @@ module.exports = function(
}
// Stop performing work
- priorityLevel = NoWork;
+ minPriorityLevel = NoWork;
// If have we more work, and we're in a deferred batch, check to see
// if the deadline has expired.
@@ -945,7 +876,7 @@ module.exports = function(
!deadlineHasExpired
) {
// We have more time to do work.
- priorityLevel = nextPriorityLevel;
+ minPriorityLevel = nextPriorityLevel;
continue;
}
@@ -956,7 +887,7 @@ module.exports = function(
case TaskPriority:
// Perform work immediately by switching the priority level
// and continuing the loop.
- priorityLevel = nextPriorityLevel;
+ minPriorityLevel = nextPriorityLevel;
break;
case AnimationPriority:
scheduleAnimationCallback(performAnimationWork);
@@ -1233,6 +1164,11 @@ module.exports = function(
return;
}
+ root.pendingWorkPriority = largerPriority(
+ root.pendingWorkPriority,
+ priorityLevel,
+ );
+
if (!root.isScheduled) {
root.isScheduled = true;
if (lastScheduledRoot) {
@@ -1266,12 +1202,14 @@ module.exports = function(
}
}
- let node = fiber;
+ let child = fiber;
+ let node = fiber.return;
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.
+ // matches) we can exit safely knowing that the rest of the path
+ // is correct.
shouldContinue = false;
if (
node.pendingWorkPriority === NoWork ||
@@ -1291,39 +1229,41 @@ module.exports = function(
node.alternate.pendingWorkPriority = priorityLevel;
}
}
- if (node.return === null) {
- if (node.tag === HostRoot) {
- const root: FiberRoot = (node.stateNode: any);
- scheduleRoot(root, priorityLevel);
- // Depending on the priority level, either perform work now or
- // schedule a callback to perform work later.
- switch (priorityLevel) {
- case SynchronousPriority:
- performWork(SynchronousPriority, null);
- return;
- case TaskPriority:
- // TODO: If we're not already performing work, schedule a
- // deferred callback.
- return;
- case AnimationPriority:
- scheduleAnimationCallback(performAnimationWork);
- return;
- case HighPriority:
- case LowPriority:
- case OffscreenPriority:
- scheduleDeferredCallback(performDeferredWork);
- return;
- }
- } else {
- if (__DEV__) {
- if (fiber.tag === ClassComponent) {
- warnAboutUpdateOnUnmounted(fiber.stateNode);
- }
+ child = node;
+ node = node.return;
+ }
+
+ if (node === null) {
+ if (child.tag === HostRoot) {
+ const root: FiberRoot = (child.stateNode: any);
+ scheduleRoot(root, priorityLevel);
+ // Depending on the priority level, either perform work now or
+ // schedule a callback to perform work later.
+ switch (priorityLevel) {
+ case SynchronousPriority:
+ performWork(SynchronousPriority, null);
+ return;
+ case TaskPriority:
+ // TODO: If we're not already performing work, schedule a
+ // deferred callback.
+ return;
+ case AnimationPriority:
+ scheduleAnimationCallback(performAnimationWork);
+ return;
+ case HighPriority:
+ case LowPriority:
+ case OffscreenPriority:
+ scheduleDeferredCallback(performDeferredWork);
+ return;
+ }
+ } else {
+ if (__DEV__) {
+ if (fiber.tag === ClassComponent) {
+ warnAboutUpdateOnUnmounted(fiber.stateNode);
}
- return;
}
+ return;
}
- node = node.return;
}
}
@@ -1356,6 +1296,13 @@ module.exports = function(
}
function scheduleErrorRecovery(fiber: Fiber) {
+ // The error boundary's children now have TaskPriority. Conceptually, this
+ // is a special case of pendingProps.
+ fiber.pendingWorkPriority = TaskPriority;
+ if (fiber.alternate) {
+ fiber.alternate.pendingWorkPriority = TaskPriority;
+ }
+ // Bubble the priority to the root
scheduleUpdate(fiber, TaskPriority);
}
diff --git a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js
index 08b19e31379d..bc591e032811 100644
--- a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js
+++ b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js
@@ -15,14 +15,14 @@
import type {Fiber} from 'ReactFiber';
import type {PriorityLevel} from 'ReactPriorityLevel';
-const {Callback: CallbackEffect} = require('ReactTypeOfSideEffect');
-
const {
NoWork,
SynchronousPriority,
TaskPriority,
} = require('ReactPriorityLevel');
+const {ClassComponent, HostRoot} = require('ReactTypeOfWork');
+
const invariant = require('fbjs/lib/invariant');
if (__DEV__) {
var warning = require('fbjs/lib/warning');
@@ -85,65 +85,19 @@ function comparePriority(a: PriorityLevel, b: PriorityLevel): number {
return a - b;
}
-// Ensures that a fiber has an update queue, creating a new one if needed.
-// Returns the new or existing queue.
-function ensureUpdateQueue(fiber: Fiber): UpdateQueue {
- if (fiber.updateQueue !== null) {
- // We already have an update queue.
- return fiber.updateQueue;
- }
-
- let queue;
+function createUpdateQueue(): UpdateQueue {
+ const queue: UpdateQueue = {
+ first: null,
+ last: null,
+ hasForceUpdate: false,
+ callbackList: null,
+ };
if (__DEV__) {
- queue = {
- first: null,
- last: null,
- hasForceUpdate: false,
- callbackList: null,
- isProcessing: false,
- };
- } else {
- queue = {
- first: null,
- last: null,
- hasForceUpdate: false,
- callbackList: null,
- };
+ queue.isProcessing = false;
}
-
- fiber.updateQueue = queue;
return queue;
}
-// Clones an update queue from a source fiber onto its alternate.
-function cloneUpdateQueue(
- current: Fiber,
- workInProgress: Fiber,
-): UpdateQueue | null {
- const currentQueue = current.updateQueue;
- if (currentQueue === null) {
- // The source fiber does not have an update queue.
- workInProgress.updateQueue = null;
- return null;
- }
- // If the alternate already has a queue, reuse the previous object.
- const altQueue = workInProgress.updateQueue !== null
- ? workInProgress.updateQueue
- : {};
- altQueue.first = currentQueue.first;
- altQueue.last = currentQueue.last;
-
- // These fields are invalid by the time we clone from current. Reset them.
- altQueue.hasForceUpdate = false;
- altQueue.callbackList = null;
- altQueue.isProcessing = false;
-
- workInProgress.updateQueue = altQueue;
-
- return altQueue;
-}
-exports.cloneUpdateQueue = cloneUpdateQueue;
-
function cloneUpdate(update: Update): Update {
return {
priorityLevel: update.priorityLevel,
@@ -204,6 +158,64 @@ function findInsertionPosition(queue, update): Update | null {
return insertAfter;
}
+function ensureUpdateQueues(fiber: Fiber): [UpdateQueue, UpdateQueue | null] {
+ const alternateFiber = fiber.alternate;
+ const progressedWork = fiber.progressedWork;
+
+ // If there are
+
+ // Ensures that the fiber, its alternate, and the progressed work object all
+ // have update queues, without overwriting an existing queue. There are up to
+ // two distinct update queues, no more.
+ let queue1;
+ let queue2;
+ if (alternateFiber === null) {
+ queue1 = fiber.updateQueue;
+ queue2 = null;
+ if (queue1 === null) {
+ queue1 = fiber.updateQueue = createUpdateQueue();
+ }
+ } else {
+ queue1 = fiber.updateQueue;
+ queue2 = alternateFiber.updateQueue;
+ if (queue1 === null && queue2 === null) {
+ queue1 = queue2 = fiber.updateQueue = alternateFiber.updateQueue = createUpdateQueue();
+ } else {
+ if (queue1 === null) {
+ queue1 = fiber.updateQueue = createUpdateQueue();
+ }
+ if (queue2 === null) {
+ queue2 = alternateFiber.updateQueue = createUpdateQueue();
+ }
+ }
+ }
+
+ if (progressedWork !== fiber && progressedWork !== alternateFiber) {
+ // We have a forked progressed work object. We need to ensure we insert
+ // updates into this queue, too.
+ //
+ // This should only happen when a fork is followed by a bailout. So both
+ // fibers should have the same queue: queue1 === queue2.
+ //
+ // An exception is if setState is called during the begin phase (render).
+ // In that case, the forked progressed work is about to be replaced by
+ // the work-in-progress. So we can ignore it.
+ if (queue1 === queue2) {
+ queue2 = progressedWork.updateQueue;
+ if (progressedWork.updateQueue === null) {
+ queue2 = progressedWork.updateQueue = createUpdateQueue();
+ }
+ }
+ }
+
+ // TODO: Refactor to avoid returning a tuple.
+ return [
+ queue1,
+ // Return null if there is no alternate queue, or if its queue is the same.
+ queue2 !== queue1 ? queue2 : null,
+ ];
+}
+
// The work-in-progress queue is a subset of the current queue (if it exists).
// We need to insert the incoming update into both lists. However, it's possible
// that the correct position in one list will be different from the position in
@@ -234,10 +246,8 @@ function findInsertionPosition(queue, update): Update | null {
//
// If the update is cloned, it returns the cloned update.
function insertUpdate(fiber: Fiber, update: Update): Update | null {
- const queue1 = ensureUpdateQueue(fiber);
- const queue2 = fiber.alternate !== null
- ? ensureUpdateQueue(fiber.alternate)
- : null;
+ // We'll have at least one and at most two distinct update queues.
+ const [queue1, queue2] = ensureUpdateQueues(fiber);
// Warn if an update is scheduled from inside an updater function.
if (__DEV__) {
@@ -274,13 +284,12 @@ function insertUpdate(fiber: Fiber, update: Update): Update | null {
// insertion positions because it mutates the list.
insertUpdateIntoQueue(queue1, update, insertAfter1, insertBefore1);
- if (insertBefore1 !== insertBefore2) {
- // 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);
- return update2;
- } else {
+ // See if the insertion positions are equal. Be careful to only compare
+ // non-null values.
+ if (
+ (insertBefore1 === insertBefore2 && insertBefore1 !== null) ||
+ (insertAfter1 === insertAfter2 && insertAfter1 !== null)
+ ) {
// The insertion positions are the same, so when we inserted into the first
// queue, it also inserted into the alternate. All we need to do is update
// the alternate queue's `first` and `last` pointers, in case they
@@ -291,9 +300,14 @@ function insertUpdate(fiber: Fiber, update: Update): Update | null {
if (insertBefore2 === null) {
queue2.last = null;
}
+ return null;
+ } else {
+ // 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);
+ return update2;
}
-
- return null;
}
function addUpdate(
@@ -352,10 +366,17 @@ function addForceUpdate(
}
exports.addForceUpdate = addForceUpdate;
-function getPendingPriority(queue: UpdateQueue): PriorityLevel {
- return queue.first !== null ? queue.first.priorityLevel : NoWork;
+function getUpdatePriority(fiber: Fiber): PriorityLevel {
+ const updateQueue = fiber.updateQueue;
+ if (updateQueue === null) {
+ return NoWork;
+ }
+ if (fiber.tag !== ClassComponent && fiber.tag !== HostRoot) {
+ return NoWork;
+ }
+ return updateQueue.first !== null ? updateQueue.first.priorityLevel : NoWork;
}
-exports.getPendingPriority = getPendingPriority;
+exports.getUpdatePriority = getUpdatePriority;
function addTopLevelUpdate(
fiber: Fiber,
@@ -377,13 +398,12 @@ function addTopLevelUpdate(
const update2 = insertUpdate(fiber, update);
if (isTopLevelUnmount) {
+ // TODO: Redesign the top-level mount/update/unmount API to avoid this
+ // special case.
+ const [queue1, queue2] = ensureUpdateQueues(fiber);
+
// Drop all updates that are lower-priority, so that the tree is not
// remounted. We need to do this for both queues.
- const queue1 = fiber.updateQueue;
- const queue2 = fiber.alternate !== null
- ? fiber.alternate.updateQueue
- : null;
-
if (queue1 !== null && update.next !== null) {
update.next = null;
queue1.last = update;
@@ -407,6 +427,7 @@ function getStateFromUpdate(update, instance, prevState, props) {
}
function beginUpdateQueue(
+ current: Fiber | null,
workInProgress: Fiber,
queue: UpdateQueue,
instance: any,
@@ -414,19 +435,33 @@ function beginUpdateQueue(
props: any,
priorityLevel: PriorityLevel,
): any {
+ if (current !== null && current.updateQueue === queue) {
+ // We need to create a work-in-progress queue, by cloning the current queue.
+ const currentQueue = queue;
+ queue = workInProgress.updateQueue = {
+ first: currentQueue.first,
+ last: currentQueue.last,
+ // These fields are no longer valid because they were already committed.
+ // Reset them.
+ callbackList: null,
+ hasForceUpdate: false,
+ };
+ }
+
if (__DEV__) {
// Set this flag so we can warn if setState is called inside the update
// function of another setState.
queue.isProcessing = true;
}
- queue.hasForceUpdate = false;
+ // Calculate these using the the existing values as a base.
+ let callbackList = queue.callbackList;
+ let hasForceUpdate = queue.hasForceUpdate;
// Applies updates with matching priority to the previous state to create
// a new state object.
let state = prevState;
let dontMutatePrevState = true;
- let callbackList = queue.callbackList;
let update = queue.first;
while (
update !== null &&
@@ -456,7 +491,7 @@ function beginUpdateQueue(
}
}
if (update.isForced) {
- queue.hasForceUpdate = true;
+ hasForceUpdate = true;
}
// Second condition ignores top-level unmount callbacks if they are not the
// last update in the queue, since a subsequent update will cause a remount.
@@ -464,21 +499,22 @@ function beginUpdateQueue(
update.callback !== null &&
!(update.isTopLevelUnmount && update.next !== null)
) {
- callbackList = callbackList || [];
+ callbackList = callbackList !== null ? callbackList : [];
callbackList.push(update.callback);
- workInProgress.effectTag |= CallbackEffect;
}
update = update.next;
}
queue.callbackList = callbackList;
+ queue.hasForceUpdate = hasForceUpdate;
- if (queue.first === null && callbackList === null && !queue.hasForceUpdate) {
+ if (queue.first === null && callbackList === null && !hasForceUpdate) {
// The queue is empty and there are no callbacks. We can reset it.
workInProgress.updateQueue = null;
}
if (__DEV__) {
+ // No longer processing.
queue.isProcessing = false;
}
@@ -495,6 +531,10 @@ function commitCallbacks(
if (callbackList === null) {
return;
}
+
+ // Set the list to null to make sure they don't get called more than once.
+ queue.callbackList = null;
+
for (let i = 0; i < callbackList.length; i++) {
const callback = callbackList[i];
invariant(
diff --git a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js
index 5bfa7066ccc2..37b4a20db463 100644
--- a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js
+++ b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js
@@ -493,7 +493,7 @@ describe('ReactIncremental', () => {
// Let us try this again without fully finishing the first time. This will
// create a hanging subtree that is reconciling at the normal priority.
ReactNoop.render();
- ReactNoop.flushDeferredPri(40);
+ ReactNoop.flushDeferredPri(35);
expect(ops).toEqual(['Foo', 'Bar']);
diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js
index 43aa4e4980ea..0b9ac7908edd 100644
--- a/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js
+++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js
@@ -795,6 +795,172 @@ describe('ReactIncrementalSideEffects', () => {
// moves to "current" without flushing due to having lower priority. Does this
// even happen? Maybe a child doesn't get processed because it is lower prio?
+ it('does not drop priority from a progressed subtree', () => {
+ let ops = [];
+ let lowPri;
+ let highPri;
+
+ function LowPriDidComplete() {
+ ops.push('LowPriDidComplete');
+ // Because this is terminal, beginning work on LowPriDidComplete implies
+ // that LowPri will be completed before the scheduler yields.
+ return null;
+ }
+
+ class LowPri extends React.Component {
+ state = {step: 0};
+ render() {
+ ops.push('LowPri');
+ lowPri = this;
+ return [
+ ,
+ ,
+ ];
+ }
+ }
+
+ function LowPriSibling() {
+ ops.push('LowPriSibling');
+ return null;
+ }
+
+ class HighPri extends React.Component {
+ state = {step: 0};
+ render() {
+ ops.push('HighPri');
+ highPri = this;
+ return ;
+ }
+ }
+
+ function App() {
+ ops.push('App');
+ return [
+
+
+
+
,
+
,
+ ];
+ }
+
+ ReactNoop.render();
+ ReactNoop.flush();
+ expect(ReactNoop.getChildren()).toEqual([div(span(0)), div(span(0))]);
+ ops = [];
+
+ lowPri.setState({step: 1});
+ // Do just enough work to begin LowPri
+ ReactNoop.flushDeferredPri(30);
+ expect(ops).toEqual(['LowPri']);
+ // Now we'll do one more tick of work to complete LowPri. Because LowPri
+ // has a sibling, the parent div of LowPri has not yet completed.
+ ReactNoop.flushDeferredPri(10);
+ expect(ops).toEqual(['LowPri', 'LowPriDidComplete']);
+ expect(ReactNoop.getChildren()).toEqual([
+ div(span(0)), // Complete, but not yet updated
+ div(span(0)),
+ ]);
+ ops = [];
+
+ // Interrupt with high pri update
+ ReactNoop.performAnimationWork(() => highPri.setState({step: 1}));
+ ReactNoop.flushAnimationPri();
+ expect(ops).toEqual(['HighPri']);
+ expect(ReactNoop.getChildren()).toEqual([
+ div(span(0)), // Completed, but not yet updated
+ div(span(1)),
+ ]);
+ ops = [];
+
+ ReactNoop.flush();
+ expect(ReactNoop.getChildren()).toEqual([div(span(1)), div(span(1))]);
+ });
+
+ it('does not complete already completed work', () => {
+ let ops = [];
+ let lowPri;
+ let highPri;
+
+ function LowPriDidComplete() {
+ ops.push('LowPriDidComplete');
+ // Because this is terminal, beginning work on LowPriDidComplete implies
+ // that LowPri will be completed before the scheduler yields.
+ return null;
+ }
+
+ class LowPri extends React.Component {
+ state = {step: 0};
+ render() {
+ ops.push('LowPri');
+ lowPri = this;
+ return [
+ ,
+ ,
+ ];
+ }
+ }
+
+ function LowPriSibling() {
+ ops.push('LowPriSibling');
+ return null;
+ }
+
+ class HighPri extends React.Component {
+ state = {step: 0};
+ render() {
+ ops.push('HighPri');
+ highPri = this;
+ return ;
+ }
+ }
+
+ function App() {
+ ops.push('App');
+ return [
+
+
+
+
,
+
,
+ ];
+ }
+
+ ReactNoop.render();
+ ReactNoop.flush();
+ expect(ReactNoop.getChildren()).toEqual([div(span(0)), div(span(0))]);
+ ops = [];
+
+ lowPri.setState({step: 1});
+ // Do just enough work to begin LowPri
+ ReactNoop.flushDeferredPri(30);
+ expect(ops).toEqual(['LowPri']);
+ // Now we'll do one more tick of work to complete LowPri. Because LowPri
+ // has a sibling, the parent div of LowPri has not yet completed.
+ ReactNoop.flushDeferredPri(10);
+ expect(ops).toEqual(['LowPri', 'LowPriDidComplete']);
+ expect(ReactNoop.getChildren()).toEqual([
+ div(span(0)), // Complete, but not yet updated
+ div(span(0)),
+ ]);
+ ops = [];
+
+ // Interrupt with high pri update
+ ReactNoop.performAnimationWork(() => highPri.setState({step: 1}));
+ ReactNoop.flushAnimationPri();
+ expect(ops).toEqual(['HighPri']);
+ expect(ReactNoop.getChildren()).toEqual([
+ div(span(0)), // Completed, but not yet updated
+ div(span(1)),
+ ]);
+ ops = [];
+
+ // If this is not enough to commit the rest of the work, that means we're
+ // not bailing out on the already-completed LowPri tree.
+ ReactNoop.flushDeferredPri(45);
+ expect(ReactNoop.getChildren()).toEqual([div(span(1)), div(span(1))]);
+ });
+
it('calls callback after update is flushed', () => {
let instance;
class Foo extends React.Component {
diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js
new file mode 100644
index 000000000000..164b8f718118
--- /dev/null
+++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js
@@ -0,0 +1,343 @@
+/**
+ * Copyright 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+var React;
+var ReactNoop;
+var ReactFeatureFlags;
+
+describe('ReactIncrementalTriangle', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ React = require('react');
+ ReactNoop = require('ReactNoop');
+
+ ReactFeatureFlags = require('ReactFeatureFlags');
+ ReactFeatureFlags.disableNewFiberFeatures = false;
+ });
+
+ function span(prop) {
+ return {type: 'span', children: [], prop};
+ }
+
+ const FLUSH = 'FLUSH';
+ function flush(unitsOfWork = Infinity) {
+ return {
+ type: FLUSH,
+ unitsOfWork,
+ };
+ }
+
+ const STEP = 'STEP';
+ function step(counter) {
+ return {
+ type: STEP,
+ counter,
+ };
+ }
+
+ const INTERRUPT = 'INTERRUPT';
+ function interrupt(key) {
+ return {
+ type: INTERRUPT,
+ };
+ }
+
+ const TOGGLE = 'TOGGLE';
+ function toggle(childIndex) {
+ return {
+ type: TOGGLE,
+ childIndex,
+ };
+ }
+
+ function TriangleSimulator() {
+ let triangles = [];
+ let leafTriangles = [];
+ let yieldAfterEachRender = false;
+ class Triangle extends React.Component {
+ constructor(props) {
+ super();
+ this.index = triangles.length;
+ triangles.push(this);
+ if (props.depth === 0) {
+ this.leafIndex = leafTriangles.length;
+ leafTriangles.push(this);
+ }
+ this.state = {isActive: false};
+ }
+ activate() {
+ if (this.props.depth !== 0) {
+ throw new Error('Cannot activate non-leaf component');
+ }
+ ReactNoop.performAnimationWork(() => {
+ this.setState({isActive: true});
+ });
+ }
+ deactivate() {
+ if (this.props.depth !== 0) {
+ throw new Error('Cannot deactivate non-leaf component');
+ }
+ ReactNoop.performAnimationWork(() => {
+ this.setState({isActive: false});
+ });
+ }
+ shouldComponentUpdate(nextProps, nextState) {
+ return (
+ this.props.counter !== nextProps.counter ||
+ this.state.isActive !== nextState.isActive
+ );
+ }
+ render() {
+ if (yieldAfterEachRender) {
+ ReactNoop.yield(this);
+ }
+ const {counter, depth} = this.props;
+ if (depth === 0) {
+ if (this.state.isActive) {
+ return ;
+ }
+ return ;
+ }
+ return [
+ ,
+ ,
+ ,
+ ];
+ }
+ }
+
+ let appInstance;
+ class App extends React.Component {
+ state = {counter: 0};
+ interrupt() {
+ // Triggers a restart from the top.
+ ReactNoop.performAnimationWork(() => {
+ this.forceUpdate();
+ });
+ }
+ setCounter(counter) {
+ const currentCounter = this.state.counter;
+ this.setState({counter});
+ return currentCounter;
+ }
+ render() {
+ appInstance = this;
+ return ;
+ }
+ }
+
+ const depth = 3;
+
+ let keyCounter = 0;
+ function reset(nextStep = 0) {
+ triangles = [];
+ leafTriangles = [];
+ // Remounts the whole tree by changing the key
+ ReactNoop.render();
+ ReactNoop.flush();
+ treeIsConsistent();
+ return appInstance;
+ }
+
+ reset();
+ const totalChildren = leafTriangles.length;
+ const totalTriangles = triangles.length;
+
+ function treeIsConsistent(activeTriangle, counter) {
+ const activeIndex = activeTriangle ? activeTriangle.leafIndex : -1;
+
+ const children = ReactNoop.getChildren();
+ for (let i = 0; i < children.length; i++) {
+ let child = children[i];
+ let num = child.prop;
+
+ // If an expected counter is not specified, use the value of the
+ // first child.
+ if (counter === undefined) {
+ if (typeof num === 'string') {
+ counter = num.substr(1, num.length - 2);
+ } else {
+ counter = num;
+ }
+ }
+
+ if (i === activeIndex) {
+ if (num !== `*${counter}*`) {
+ throw new Error(
+ `Triangle ${i} is inconsistent: ${num} instead of *${counter}*.`,
+ );
+ }
+ } else {
+ if (num !== counter) {
+ throw new Error(
+ `Triangle ${i} is inconsistent: ${num} instead of ${counter}.`,
+ );
+ }
+ }
+ }
+ }
+
+ function simulate(...actions) {
+ const app = reset();
+ let expectedCounterAtEnd = app.state.counter;
+
+ let activeTriangle = null;
+ for (let i = 0; i < actions.length; i++) {
+ const action = actions[i];
+ switch (action.type) {
+ case FLUSH:
+ ReactNoop.flushUnitsOfWork(action.unitsOfWork);
+ break;
+ case STEP:
+ app.setCounter(action.counter);
+ expectedCounterAtEnd = action.counter;
+ break;
+ case INTERRUPT:
+ app.interrupt();
+ break;
+ case TOGGLE:
+ const targetTriangle = leafTriangles[action.childIndex];
+ if (targetTriangle === undefined) {
+ throw new Error('Target index is out of bounds');
+ }
+ if (targetTriangle === activeTriangle) {
+ activeTriangle = null;
+ targetTriangle.deactivate();
+ } else {
+ if (activeTriangle !== null) {
+ activeTriangle.deactivate();
+ }
+ activeTriangle = targetTriangle;
+ targetTriangle.activate();
+ }
+ ReactNoop.flushAnimationPri();
+ break;
+ default:
+ break;
+ }
+ }
+ // Flush remaining work
+ ReactNoop.flush();
+ treeIsConsistent(activeTriangle, expectedCounterAtEnd);
+ }
+
+ return {simulate, totalChildren, totalTriangles};
+ }
+
+ it('works', () => {
+ const {simulate} = TriangleSimulator();
+ simulate(step(1));
+ simulate(toggle(0), step(1), toggle(0));
+ simulate(step(1), toggle(0), flush(2), step(2), toggle(0));
+ });
+
+ it('resumes work by comparing the priority at which the work-in-progress was created/updated', () => {
+ const {simulate} = TriangleSimulator();
+ simulate(
+ // Start a low priority update.
+ step(1),
+ // Flush part of the tree
+ flush(50),
+ // Interrupt with a high priority update to a leaf node. Part of the
+ // work from the low-pri update (step 1) overlaps with this high-pri
+ // update, but some of it is untouched.
+ toggle(17),
+ // Start a new low priority update that is the same as the current
+ // value. This should override all of the previous work. The final
+ // value of the children should be 0.
+ step(0),
+ );
+ });
+
+ xit('fuzz tester', () => {
+ // This test is not deterministic because the inputs are randomized. It runs
+ // a limited number of tests on every run. If it fails, it will output the
+ // case that led to the failure. Add the failing case to the test above
+ // to prevent future regressions.
+ const {simulate, totalTriangles, totalChildren} = TriangleSimulator();
+
+ const limit = 1000;
+
+ function randomInteger(min, max) {
+ min = Math.ceil(min);
+ max = Math.floor(max);
+ return Math.floor(Math.random() * (max - min)) + min;
+ }
+
+ function randomAction() {
+ switch (randomInteger(0, 4)) {
+ case 0:
+ return flush(randomInteger(0, totalTriangles * 1.5));
+ case 1:
+ return step(randomInteger(0, 10));
+ case 2:
+ return interrupt();
+ case 3:
+ return toggle(randomInteger(0, totalChildren));
+ default:
+ throw new Error('Switch statement should be exhaustive');
+ }
+ }
+
+ function randomActions(n) {
+ let actions = [];
+ for (let i = 0; i < n; i++) {
+ actions.push(randomAction());
+ }
+ return actions;
+ }
+
+ function formatActions(actions) {
+ let result = 'simulate(';
+ for (let i = 0; i < actions.length; i++) {
+ const action = actions[i];
+ switch (action.type) {
+ case FLUSH:
+ result += `flush(${action.unitsOfWork})`;
+ break;
+ case STEP:
+ result += `step(${action.counter})`;
+ break;
+ case INTERRUPT:
+ result += 'interrupt()';
+ break;
+ case TOGGLE:
+ result += `toggle(${action.childIndex})`;
+ break;
+ default:
+ throw new Error('Switch statement should be exhaustive');
+ }
+ if (i !== actions.length - 1) {
+ result += ', ';
+ }
+ }
+ result += ')';
+ return result;
+ }
+
+ for (let i = 0; i < limit; i++) {
+ const actions = randomActions(5);
+ try {
+ simulate(...actions);
+ } catch (e) {
+ console.error(
+ `
+Triangle fuzz tester error! Copy and paste the following line into the test suite:
+ ${formatActions(actions)}
+ `,
+ );
+ throw e;
+ }
+ }
+ });
+});
diff --git a/src/renderers/shared/fiber/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap b/src/renderers/shared/fiber/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap
index 760598a4ce5c..42a093ad6340 100644
--- a/src/renderers/shared/fiber/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap
+++ b/src/renderers/shared/fiber/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap
@@ -85,6 +85,8 @@ exports[`ReactDebugFiberPerf does not treat setState from cWM or cWRP as cascadi
⚛ (Committing Changes)
⚛ (Committing Host Effects: 2 Total)
⚛ (Calling Lifecycle Methods: 2 Total)
+
+⚛ (React Tree Reconciliation)
"
`;
@@ -212,7 +214,7 @@ exports[`ReactDebugFiberPerf supports coroutines 1`] = `
⚛ Continuation [mount]
⚛ Continuation [mount]
⚛ (Committing Changes)
- ⚛ (Committing Host Effects: 3 Total)
+ ⚛ (Committing Host Effects: 1 Total)
⚛ (Calling Lifecycle Methods: 0 Total)
"
`;