mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge pull request #7636 from sebmarkbage/fiberrefactor
[Fiber] Refactor Pending Work Phase and Progressed Work
(cherry picked from commit a19fede67c)
This commit is contained in:
committed by
Paul O’Shannessy
parent
d5059c91f5
commit
3bc9d9caa7
@@ -170,16 +170,12 @@ var ReactNoop = {
|
||||
' '.repeat(depth) + '- ' + (fiber.type ? fiber.type.name || fiber.type : '[root]'),
|
||||
'[' + fiber.pendingWorkPriority + (fiber.pendingProps ? '*' : '') + ']'
|
||||
);
|
||||
const childInProgress = fiber.childInProgress;
|
||||
if (childInProgress) {
|
||||
if (childInProgress === fiber.child) {
|
||||
console.log(' '.repeat(depth + 1) + 'ERROR: IN PROGRESS == CURRENT');
|
||||
} else {
|
||||
console.log(' '.repeat(depth + 1) + 'IN PROGRESS');
|
||||
logFiber(childInProgress, depth + 1);
|
||||
if (fiber.child) {
|
||||
console.log(' '.repeat(depth + 1) + 'CURRENT');
|
||||
}
|
||||
const childInProgress = fiber.progressedChild;
|
||||
if (childInProgress && childInProgress !== fiber.child) {
|
||||
console.log(' '.repeat(depth + 1) + 'IN PROGRESS: ' + fiber.progressedPriority);
|
||||
logFiber(childInProgress, depth + 1);
|
||||
if (fiber.child) {
|
||||
console.log(' '.repeat(depth + 1) + 'CURRENT');
|
||||
}
|
||||
}
|
||||
if (fiber.child) {
|
||||
|
||||
@@ -63,10 +63,10 @@ function ChildReconciler(shouldClone) {
|
||||
// Will fix reconciliation properly later.
|
||||
const clone = shouldClone ? cloneFiber(existingChild, priority) : existingChild;
|
||||
if (!shouldClone) {
|
||||
// TODO: This might be lowering the priority of nested unfinished work.
|
||||
clone.pendingWorkPriority = priority;
|
||||
}
|
||||
clone.pendingProps = element.props;
|
||||
clone.child = existingChild.child;
|
||||
clone.sibling = null;
|
||||
clone.return = returnFiber;
|
||||
previousSibling.sibling = clone;
|
||||
@@ -134,10 +134,10 @@ function ChildReconciler(shouldClone) {
|
||||
// Get the clone of the existing fiber.
|
||||
const clone = shouldClone ? cloneFiber(existingChild, priority) : existingChild;
|
||||
if (!shouldClone) {
|
||||
// TODO: This might be lowering the priority of nested unfinished work.
|
||||
clone.pendingWorkPriority = priority;
|
||||
}
|
||||
clone.pendingProps = element.props;
|
||||
clone.child = existingChild.child;
|
||||
clone.sibling = null;
|
||||
clone.return = returnFiber;
|
||||
return clone;
|
||||
@@ -219,3 +219,49 @@ function ChildReconciler(shouldClone) {
|
||||
exports.reconcileChildFibers = ChildReconciler(true);
|
||||
|
||||
exports.reconcileChildFibersInPlace = ChildReconciler(false);
|
||||
|
||||
|
||||
function cloneSiblings(current : Fiber, workInProgress : Fiber, returnFiber : Fiber) {
|
||||
workInProgress.return = returnFiber;
|
||||
while (current.sibling) {
|
||||
current = current.sibling;
|
||||
workInProgress = workInProgress.sibling = cloneFiber(
|
||||
current,
|
||||
current.pendingWorkPriority
|
||||
);
|
||||
workInProgress.return = returnFiber;
|
||||
}
|
||||
workInProgress.sibling = null;
|
||||
}
|
||||
|
||||
exports.cloneChildFibers = function(current : ?Fiber, workInProgress : Fiber) {
|
||||
if (!workInProgress.child) {
|
||||
return;
|
||||
}
|
||||
if (current && 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.
|
||||
const 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.
|
||||
const newChild = cloneFiber(currentChild, currentChild.pendingWorkPriority);
|
||||
workInProgress.child = newChild;
|
||||
cloneSiblings(currentChild, newChild, workInProgress);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
child.return = workInProgress;
|
||||
child = child.sibling;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -89,25 +89,44 @@ export type Fiber = Instance & {
|
||||
firstEffect: ?Fiber,
|
||||
lastEffect: ?Fiber,
|
||||
|
||||
|
||||
// This will be used to quickly determine if a subtree has no pending changes.
|
||||
pendingWorkPriority: PriorityLevel,
|
||||
|
||||
// This value represents the priority level that was last used to process this
|
||||
// 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,
|
||||
|
||||
// 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
|
||||
// memory if we need to.
|
||||
alternate: ?Fiber,
|
||||
|
||||
// Keeps track of the children that are currently being processed but have not
|
||||
// yet completed.
|
||||
childInProgress: ?Fiber,
|
||||
|
||||
// Conceptual aliases
|
||||
// workInProgress : Fiber -> alternate The alternate used for reuse happens
|
||||
// to be the same as work in progress.
|
||||
|
||||
};
|
||||
|
||||
// This is a constructor of a POJO instead of a constructor function for a few
|
||||
// reasons:
|
||||
// 1) Nobody should add any instance methods on this. Instance methods can be
|
||||
// more difficult to predict when they get optimized and they are almost
|
||||
// never inlined properly in static compilers.
|
||||
// 2) Nobody should rely on `instanceof Fiber` for type testing. We should
|
||||
// always know when it is a fiber.
|
||||
// 3) We can easily go from a createFiber call to calling a constructor if that
|
||||
// is faster. The opposite is not true.
|
||||
// 4) We might want to experiment with using numeric keys since they are easier
|
||||
// to optimize in a non-JIT environment.
|
||||
// 5) It should be easy to port this to a C struct and keep a C implementation
|
||||
// compatible.
|
||||
var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber {
|
||||
return {
|
||||
|
||||
@@ -139,8 +158,8 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber {
|
||||
lastEffect: null,
|
||||
|
||||
pendingWorkPriority: NoWork,
|
||||
|
||||
childInProgress: null,
|
||||
progressedPriority: NoWork,
|
||||
progressedChild: null,
|
||||
|
||||
alternate: null,
|
||||
|
||||
@@ -152,7 +171,16 @@ function shouldConstruct(Component) {
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
// 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
|
||||
@@ -161,13 +189,15 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
|
||||
let alt = fiber.alternate;
|
||||
if (alt) {
|
||||
alt.stateNode = fiber.stateNode;
|
||||
alt.child = fiber.child;
|
||||
alt.childInProgress = fiber.childInProgress;
|
||||
alt.sibling = fiber.sibling;
|
||||
alt.ref = alt.ref;
|
||||
alt.pendingProps = fiber.pendingProps;
|
||||
alt.sibling = fiber.sibling; // This should always be overridden. TODO: null
|
||||
alt.ref = fiber.ref;
|
||||
alt.pendingProps = fiber.pendingProps; // TODO: Pass as argument.
|
||||
alt.pendingWorkPriority = priorityLevel;
|
||||
|
||||
alt.child = fiber.child;
|
||||
alt.memoizedProps = fiber.memoizedProps;
|
||||
alt.output = fiber.output;
|
||||
|
||||
// Whenever we clone, we do so to get a new work in progress.
|
||||
// This ensures that we've reset these in the new tree.
|
||||
alt.nextEffect = null;
|
||||
@@ -182,13 +212,19 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
|
||||
alt.type = fiber.type;
|
||||
alt.stateNode = fiber.stateNode;
|
||||
alt.child = fiber.child;
|
||||
alt.childInProgress = fiber.childInProgress;
|
||||
alt.sibling = fiber.sibling;
|
||||
alt.ref = alt.ref;
|
||||
alt.sibling = fiber.sibling; // This should always be overridden. TODO: null
|
||||
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;
|
||||
alt.pendingWorkPriority = priorityLevel;
|
||||
|
||||
alt.memoizedProps = fiber.memoizedProps;
|
||||
alt.output = fiber.output;
|
||||
|
||||
alt.progressedChild = fiber.progressedChild;
|
||||
alt.progressedPriority = fiber.progressedPriority;
|
||||
|
||||
alt.alternate = fiber;
|
||||
fiber.alternate = alt;
|
||||
return alt;
|
||||
|
||||
@@ -15,10 +15,12 @@
|
||||
import type { ReactCoroutine } from 'ReactCoroutine';
|
||||
import type { Fiber } from 'ReactFiber';
|
||||
import type { HostConfig } from 'ReactFiberReconciler';
|
||||
import type { PriorityLevel } from 'ReactPriorityLevel';
|
||||
|
||||
var {
|
||||
reconcileChildFibers,
|
||||
reconcileChildFibersInPlace,
|
||||
cloneChildFibers,
|
||||
} = require('ReactChildFiber');
|
||||
var ReactTypeOfWork = require('ReactTypeOfWork');
|
||||
var {
|
||||
@@ -35,48 +37,72 @@ var {
|
||||
NoWork,
|
||||
OffscreenPriority,
|
||||
} = require('ReactPriorityLevel');
|
||||
var { findNextUnitOfWorkAtPriority } = require('ReactFiberPendingWork');
|
||||
|
||||
module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
|
||||
|
||||
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) {
|
||||
// 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 reconcileChildren(current, workInProgress, nextChildren) {
|
||||
const priority = workInProgress.pendingWorkPriority;
|
||||
reconcileChildrenAtPriority(current, workInProgress, nextChildren, priority);
|
||||
const priorityLevel = workInProgress.pendingWorkPriority;
|
||||
reconcileChildrenAtPriority(current, workInProgress, nextChildren, priorityLevel);
|
||||
}
|
||||
|
||||
function reconcileChildrenAtPriority(current, workInProgress, nextChildren, priorityLevel) {
|
||||
if (current && current.childInProgress) {
|
||||
workInProgress.childInProgress = reconcileChildFibersInPlace(
|
||||
// At this point any memoization is no longer valid since we'll have changed
|
||||
// the children.
|
||||
workInProgress.memoizedProps = null;
|
||||
if (current && 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.
|
||||
workInProgress.child = reconcileChildFibers(
|
||||
workInProgress,
|
||||
current.childInProgress,
|
||||
nextChildren,
|
||||
priorityLevel
|
||||
);
|
||||
// This is now invalid because we reused nodes.
|
||||
current.childInProgress = null;
|
||||
} else if (workInProgress.childInProgress) {
|
||||
workInProgress.childInProgress = reconcileChildFibersInPlace(
|
||||
workInProgress,
|
||||
workInProgress.childInProgress,
|
||||
workInProgress.child,
|
||||
nextChildren,
|
||||
priorityLevel
|
||||
);
|
||||
} else {
|
||||
workInProgress.childInProgress = reconcileChildFibers(
|
||||
// If, on the other hand, we don't have a current fiber or if 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,
|
||||
current ? current.child : null,
|
||||
workInProgress.child,
|
||||
nextChildren,
|
||||
priorityLevel
|
||||
);
|
||||
}
|
||||
markChildAsProgressed(current, workInProgress, priorityLevel);
|
||||
}
|
||||
|
||||
function updateFunctionalComponent(current, workInProgress) {
|
||||
var fn = workInProgress.type;
|
||||
var props = workInProgress.pendingProps;
|
||||
|
||||
// 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') {
|
||||
if (workInProgress.memoizedProps !== null) {
|
||||
if (!fn.shouldComponentUpdate(workInProgress.memoizedProps, props)) {
|
||||
return bailoutOnAlreadyFinishedWork(current, workInProgress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var nextChildren = fn(props);
|
||||
reconcileChildren(current, workInProgress, nextChildren);
|
||||
workInProgress.pendingWorkPriority = NoWork;
|
||||
return workInProgress.child;
|
||||
}
|
||||
|
||||
function updateClassComponent(current : ?Fiber, workInProgress : Fiber) {
|
||||
@@ -86,14 +112,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
|
||||
var ctor = workInProgress.type;
|
||||
workInProgress.stateNode = instance = new ctor(props);
|
||||
} else if (typeof instance.shouldComponentUpdate === 'function') {
|
||||
if (current && current.memoizedProps) {
|
||||
// Revert to the last flushed props, incase we aborted an update.
|
||||
instance.props = current.memoizedProps;
|
||||
if (!instance.shouldComponentUpdate(props)) {
|
||||
return bailoutOnCurrent(current, workInProgress);
|
||||
}
|
||||
}
|
||||
if (!workInProgress.childInProgress && workInProgress.memoizedProps) {
|
||||
if (workInProgress.memoizedProps !== null) {
|
||||
// Reset the props, in case this is a ping-pong case rather than a
|
||||
// completed update case. For the completed update case, the instance
|
||||
// props will already be the memoizedProps.
|
||||
@@ -103,34 +122,40 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
instance.props = props;
|
||||
var nextChildren = instance.render();
|
||||
reconcileChildren(current, workInProgress, nextChildren);
|
||||
workInProgress.pendingWorkPriority = NoWork;
|
||||
return workInProgress.childInProgress;
|
||||
|
||||
return workInProgress.child;
|
||||
}
|
||||
|
||||
function updateHostComponent(current, workInProgress) {
|
||||
var nextChildren = workInProgress.pendingProps.children;
|
||||
const nextChildren = workInProgress.pendingProps.children;
|
||||
if (workInProgress.pendingProps.hidden &&
|
||||
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.
|
||||
|
||||
let priority = workInProgress.pendingWorkPriority;
|
||||
if (workInProgress.pendingProps.hidden && priority !== OffscreenPriority) {
|
||||
// If this host component is hidden, we can reconcile its children at
|
||||
// the lowest priority and bail out from this particular pass. Unless, we're
|
||||
// currently reconciling the lowest priority.
|
||||
// If we have a child in progress already, we reconcile against that set
|
||||
// to retain any work within it. We'll recreate any component that was in
|
||||
// the current set and next set but not in the previous in progress set.
|
||||
// TODO: This attaches a node that hasn't completed rendering so it
|
||||
// becomes part of the render tree, even though it never completed. Its
|
||||
// `output` property is unpredictable because of it.
|
||||
// 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(current, workInProgress, nextChildren, OffscreenPriority);
|
||||
workInProgress.pendingWorkPriority = OffscreenPriority;
|
||||
workInProgress.child = current ? current.child : null;
|
||||
// Abort and don't process children yet.
|
||||
return null;
|
||||
} else {
|
||||
reconcileChildren(current, workInProgress, nextChildren);
|
||||
workInProgress.pendingWorkPriority = NoWork;
|
||||
return workInProgress.childInProgress;
|
||||
return workInProgress.child;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,19 +166,19 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
|
||||
if (typeof value === 'object' && value && typeof value.render === 'function') {
|
||||
// Proceed under the assumption that this is a class instance
|
||||
workInProgress.tag = ClassComponent;
|
||||
if (workInProgress.alternate) {
|
||||
workInProgress.alternate.tag = ClassComponent;
|
||||
if (current) {
|
||||
current.tag = ClassComponent;
|
||||
}
|
||||
value = value.render();
|
||||
} else {
|
||||
// Proceed under the assumption that this is a functional component
|
||||
workInProgress.tag = FunctionalComponent;
|
||||
if (workInProgress.alternate) {
|
||||
workInProgress.alternate.tag = FunctionalComponent;
|
||||
if (current) {
|
||||
current.tag = FunctionalComponent;
|
||||
}
|
||||
}
|
||||
reconcileChildren(current, workInProgress, value);
|
||||
workInProgress.pendingWorkPriority = NoWork;
|
||||
return workInProgress.child;
|
||||
}
|
||||
|
||||
function updateCoroutineComponent(current, workInProgress) {
|
||||
@@ -162,29 +187,9 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
|
||||
throw new Error('Should be resolved by now');
|
||||
}
|
||||
reconcileChildren(current, workInProgress, coroutine.children);
|
||||
workInProgress.pendingWorkPriority = NoWork;
|
||||
}
|
||||
|
||||
function reuseChildren(returnFiber : Fiber, firstChild : Fiber) {
|
||||
// TODO: None of this should be necessary if structured better.
|
||||
// The returnFiber pointer only needs to be updated when we walk into this child
|
||||
// which we don't do right now. If the pending work priority indicated only
|
||||
// if a child has work rather than if the node has work, then we would know
|
||||
// by a single lookup on workInProgress rather than having to go through
|
||||
// each child.
|
||||
let child = firstChild;
|
||||
do {
|
||||
// Update the returnFiber of the child to the newest fiber.
|
||||
child.return = returnFiber;
|
||||
// Retain the priority if there's any work left to do in the children.
|
||||
if (child.pendingWorkPriority !== NoWork &&
|
||||
(returnFiber.pendingWorkPriority === NoWork ||
|
||||
returnFiber.pendingWorkPriority > child.pendingWorkPriority)) {
|
||||
returnFiber.pendingWorkPriority = child.pendingWorkPriority;
|
||||
}
|
||||
} while (child = child.sibling);
|
||||
}
|
||||
|
||||
/*
|
||||
function reuseChildrenEffects(returnFiber : Fiber, firstChild : Fiber) {
|
||||
let child = firstChild;
|
||||
do {
|
||||
@@ -201,113 +206,81 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
|
||||
}
|
||||
} while (child = child.sibling);
|
||||
}
|
||||
|
||||
function bailoutOnCurrent(current : Fiber, workInProgress : Fiber) : ?Fiber {
|
||||
// The most likely scenario is that the previous copy of the tree contains
|
||||
// the same props as the new one. In that case, we can just copy the output
|
||||
// and children from that node.
|
||||
workInProgress.memoizedProps = workInProgress.pendingProps;
|
||||
workInProgress.output = current.output;
|
||||
const priorityLevel = workInProgress.pendingWorkPriority;
|
||||
workInProgress.pendingProps = null;
|
||||
workInProgress.pendingWorkPriority = NoWork;
|
||||
workInProgress.stateNode = current.stateNode;
|
||||
workInProgress.childInProgress = current.childInProgress;
|
||||
if (current.child) {
|
||||
// If we bail out but still has work with the current priority in this
|
||||
// subtree, we need to go find it right now. If we don't, we won't flush
|
||||
// it until the next tick.
|
||||
workInProgress.child = current.child;
|
||||
reuseChildren(workInProgress, workInProgress.child);
|
||||
if (workInProgress.pendingWorkPriority !== NoWork && workInProgress.pendingWorkPriority <= priorityLevel) {
|
||||
// TODO: This passes the current node and reads the priority level and
|
||||
// pending props from that. We want it to read our priority level and
|
||||
// pending props from the work in progress. Needs restructuring.
|
||||
return findNextUnitOfWorkAtPriority(current, priorityLevel);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
workInProgress.child = null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
function bailoutOnAlreadyFinishedWork(current, workInProgress : Fiber) : ?Fiber {
|
||||
// If we started this work before, and finished it, or if we're in a
|
||||
// ping-pong update scenario, this version could already be what we're
|
||||
// looking for. In that case, we should be able to just bail out.
|
||||
const priorityLevel = workInProgress.pendingWorkPriority;
|
||||
workInProgress.pendingProps = null;
|
||||
workInProgress.pendingWorkPriority = NoWork;
|
||||
|
||||
workInProgress.firstEffect = null;
|
||||
workInProgress.nextEffect = null;
|
||||
workInProgress.lastEffect = null;
|
||||
// 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 (workInProgress.child) {
|
||||
// On the way up here, we reset the child node to be the current one by
|
||||
// cloning. However, it is really the original child that represents the
|
||||
// already completed work. Therefore we have to reuse the alternate.
|
||||
// But if we don't have a current, this was not cloned. This is super weird.
|
||||
const child = !current ? workInProgress.child : workInProgress.child.alternate;
|
||||
if (!child) {
|
||||
throw new Error('We must have a current child to be able to use this.');
|
||||
}
|
||||
workInProgress.child = child;
|
||||
// Ensure that the effects of reused work are preserved.
|
||||
reuseChildrenEffects(workInProgress, child);
|
||||
// If we bail out but still has work with the current priority in this
|
||||
// subtree, we need to go find it right now. If we don't, we won't flush
|
||||
// it until the next tick.
|
||||
reuseChildren(workInProgress, child);
|
||||
if (workInProgress.pendingWorkPriority !== NoWork &&
|
||||
workInProgress.pendingWorkPriority <= priorityLevel) {
|
||||
// TODO: This passes the current node and reads the priority level and
|
||||
// pending props from that. We want it to read our priority level and
|
||||
// pending props from the work in progress. Needs restructuring.
|
||||
return findNextUnitOfWorkAtPriority(workInProgress, priorityLevel);
|
||||
}
|
||||
cloneChildFibers(current, workInProgress);
|
||||
markChildAsProgressed(current, workInProgress, priorityLevel);
|
||||
return workInProgress.child;
|
||||
}
|
||||
|
||||
function bailoutOnLowPriority(current, workInProgress) {
|
||||
if (current) {
|
||||
workInProgress.child = current.child;
|
||||
workInProgress.memoizedProps = current.memoizedProps;
|
||||
workInProgress.output = current.output;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function beginWork(current : ?Fiber, workInProgress : Fiber) : ?Fiber {
|
||||
// The current, flushed, state of this fiber is the alternate.
|
||||
// Ideally nothing should rely on this, but relying on it here
|
||||
// means that we don't need an additional field on the work in
|
||||
// progress.
|
||||
if (current && workInProgress.pendingProps === current.memoizedProps) {
|
||||
return bailoutOnCurrent(current, workInProgress);
|
||||
function beginWork(current : ?Fiber, workInProgress : Fiber, priorityLevel : PriorityLevel) : ?Fiber {
|
||||
if (workInProgress.pendingWorkPriority === NoWork ||
|
||||
workInProgress.pendingWorkPriority > priorityLevel) {
|
||||
return bailoutOnLowPriority(current, workInProgress);
|
||||
}
|
||||
|
||||
if (!workInProgress.childInProgress &&
|
||||
workInProgress.pendingProps === workInProgress.memoizedProps) {
|
||||
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;
|
||||
}
|
||||
|
||||
if (workInProgress.pendingProps === null || (
|
||||
workInProgress.memoizedProps !== null &&
|
||||
workInProgress.pendingProps === workInProgress.memoizedProps
|
||||
)) {
|
||||
return bailoutOnAlreadyFinishedWork(current, workInProgress);
|
||||
}
|
||||
|
||||
switch (workInProgress.tag) {
|
||||
case IndeterminateComponent:
|
||||
mountIndeterminateComponent(current, workInProgress);
|
||||
return workInProgress.childInProgress;
|
||||
return mountIndeterminateComponent(current, workInProgress);
|
||||
case FunctionalComponent:
|
||||
updateFunctionalComponent(current, workInProgress);
|
||||
return workInProgress.childInProgress;
|
||||
return updateFunctionalComponent(current, workInProgress);
|
||||
case ClassComponent:
|
||||
return updateClassComponent(current, workInProgress);
|
||||
case HostContainer:
|
||||
reconcileChildren(current, workInProgress, workInProgress.pendingProps);
|
||||
// A yield component is just a placeholder, we can just run through the
|
||||
// next one immediately.
|
||||
workInProgress.pendingWorkPriority = NoWork;
|
||||
if (workInProgress.childInProgress) {
|
||||
if (workInProgress.child) {
|
||||
return beginWork(
|
||||
workInProgress.childInProgress.alternate,
|
||||
workInProgress.childInProgress
|
||||
workInProgress.child.alternate,
|
||||
workInProgress.child,
|
||||
priorityLevel
|
||||
);
|
||||
}
|
||||
return null;
|
||||
case HostComponent:
|
||||
if (workInProgress.stateNode && config.beginUpdate) {
|
||||
config.beginUpdate(workInProgress.stateNode);
|
||||
}
|
||||
return updateHostComponent(current, workInProgress);
|
||||
case CoroutineHandlerPhase:
|
||||
// This is a restart. Reset the tag to the initial phase.
|
||||
@@ -317,21 +290,22 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
|
||||
updateCoroutineComponent(current, workInProgress);
|
||||
// This doesn't take arbitrary time so we could synchronously just begin
|
||||
// eagerly do the work of workInProgress.child as an optimization.
|
||||
if (workInProgress.childInProgress) {
|
||||
if (workInProgress.child) {
|
||||
return beginWork(
|
||||
workInProgress.childInProgress.alternate,
|
||||
workInProgress.childInProgress
|
||||
workInProgress.child.alternate,
|
||||
workInProgress.child,
|
||||
priorityLevel
|
||||
);
|
||||
}
|
||||
return workInProgress.childInProgress;
|
||||
return workInProgress.child;
|
||||
case YieldComponent:
|
||||
// A yield component is just a placeholder, we can just run through the
|
||||
// next one immediately.
|
||||
workInProgress.pendingWorkPriority = NoWork;
|
||||
if (workInProgress.sibling) {
|
||||
return beginWork(
|
||||
workInProgress.sibling.alternate,
|
||||
workInProgress.sibling
|
||||
workInProgress.sibling,
|
||||
priorityLevel
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -28,7 +28,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
|
||||
const updateContainer = config.updateContainer;
|
||||
const commitUpdate = config.commitUpdate;
|
||||
|
||||
function commitWork(finishedWork : Fiber) : void {
|
||||
function commitWork(current : ?Fiber, finishedWork : Fiber) : void {
|
||||
switch (finishedWork.tag) {
|
||||
case ClassComponent: {
|
||||
// TODO: Fire componentDidMount/componentDidUpdate, update refs
|
||||
@@ -43,14 +43,13 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
|
||||
return;
|
||||
}
|
||||
case HostComponent: {
|
||||
if (finishedWork.stateNode == null || !finishedWork.alternate) {
|
||||
if (finishedWork.stateNode == null || !current) {
|
||||
throw new Error('This should only be done during updates.');
|
||||
}
|
||||
// Commit the work prepared earlier.
|
||||
const child = finishedWork.child;
|
||||
const children = (child && !child.sibling) ? (child.output : ?Fiber | I) : child;
|
||||
const newProps = finishedWork.memoizedProps;
|
||||
const current = finishedWork.alternate;
|
||||
const oldProps = current.memoizedProps;
|
||||
const instance : I = finishedWork.stateNode;
|
||||
commitUpdate(instance, oldProps, newProps, children);
|
||||
|
||||
@@ -162,10 +162,16 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
|
||||
// This returns true if there was something to update.
|
||||
markForPreEffect(workInProgress);
|
||||
}
|
||||
// TODO: Is this actually ever going to change? Why set it every time?
|
||||
workInProgress.output = instance;
|
||||
} else {
|
||||
if (!newProps) {
|
||||
throw new Error('We must have new props for new mounts.');
|
||||
if (workInProgress.stateNode === null) {
|
||||
throw new Error('We must have new props for new mounts.');
|
||||
} else {
|
||||
// This can happen when we abort work.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const instance = createInstance(workInProgress.type, newProps, children);
|
||||
// TODO: This seems like unnecessary duplication.
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @providesModule ReactFiberPendingWork
|
||||
* @flow
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import type { Fiber } from 'ReactFiber';
|
||||
import type { PriorityLevel } from 'ReactPriorityLevel';
|
||||
|
||||
var { cloneFiber } = require('ReactFiber');
|
||||
|
||||
var {
|
||||
NoWork,
|
||||
} = require('ReactPriorityLevel');
|
||||
|
||||
function cloneSiblings(current : Fiber, workInProgress : Fiber, returnFiber : Fiber) {
|
||||
workInProgress.return = returnFiber;
|
||||
while (current.sibling) {
|
||||
current = current.sibling;
|
||||
workInProgress = workInProgress.sibling = cloneFiber(
|
||||
current,
|
||||
current.pendingWorkPriority
|
||||
);
|
||||
workInProgress.return = returnFiber;
|
||||
}
|
||||
workInProgress.sibling = null;
|
||||
}
|
||||
|
||||
exports.findNextUnitOfWorkAtPriority = function(currentRoot : Fiber, priorityLevel : PriorityLevel) : ?Fiber {
|
||||
let current = currentRoot;
|
||||
while (current) {
|
||||
if (current.pendingWorkPriority !== NoWork &&
|
||||
current.pendingWorkPriority <= priorityLevel) {
|
||||
// This node has work to do that fits our priority level criteria.
|
||||
if (current.pendingProps !== null) {
|
||||
// We found some work to do. We need to return the "work in progress"
|
||||
// of this node which will be the alternate.
|
||||
const workInProgress = current.alternate;
|
||||
if (!workInProgress) {
|
||||
throw new Error('Should have wip now');
|
||||
}
|
||||
workInProgress.pendingProps = current.pendingProps;
|
||||
return workInProgress;
|
||||
}
|
||||
|
||||
// If we have a child let's see if any of our children has work to do.
|
||||
// Only bother doing this at all if the current priority level matches
|
||||
// because it is the highest priority for the whole subtree.
|
||||
// TODO: Coroutines can have work in their stateNode which is another
|
||||
// type of child that needs to be searched for work.
|
||||
if (current.childInProgress) {
|
||||
let workInProgress = current.childInProgress;
|
||||
while (workInProgress) {
|
||||
workInProgress.return = current.alternate;
|
||||
workInProgress = workInProgress.sibling;
|
||||
}
|
||||
workInProgress = current.childInProgress;
|
||||
while (workInProgress) {
|
||||
// Don't bother drilling further down this tree if there is no child.
|
||||
if (workInProgress.pendingWorkPriority !== NoWork &&
|
||||
workInProgress.pendingWorkPriority <= priorityLevel &&
|
||||
workInProgress.pendingProps !== null) {
|
||||
return workInProgress;
|
||||
}
|
||||
workInProgress = workInProgress.sibling;
|
||||
}
|
||||
} else if (current.child) {
|
||||
let currentChild = current.child;
|
||||
currentChild.return = current;
|
||||
// Ensure we have a work in progress copy to backtrack through.
|
||||
let workInProgress = current.alternate;
|
||||
if (!workInProgress) {
|
||||
throw new Error('Should have wip now');
|
||||
}
|
||||
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.
|
||||
workInProgress.child = cloneFiber(
|
||||
currentChild,
|
||||
currentChild.pendingWorkPriority
|
||||
);
|
||||
cloneSiblings(currentChild, workInProgress.child, workInProgress);
|
||||
current = currentChild;
|
||||
continue;
|
||||
}
|
||||
// If we match the priority but has no child and no work to do,
|
||||
// then we can safely reset the flag.
|
||||
current.pendingWorkPriority = NoWork;
|
||||
}
|
||||
if (current === currentRoot) {
|
||||
if (current.pendingWorkPriority <= priorityLevel) {
|
||||
// If this subtree had work left to do, we would have returned it by
|
||||
// now. This could happen if a child with pending work gets cleaned up
|
||||
// but we don't clear the flag then. It is safe to reset it now.
|
||||
current.pendingWorkPriority = NoWork;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
while (!current.sibling) {
|
||||
current = current.return;
|
||||
if (!current) {
|
||||
return null;
|
||||
}
|
||||
if (current.pendingWorkPriority <= priorityLevel) {
|
||||
// If this subtree had work left to do, we would have returned it by
|
||||
// now. This could happen if a child with pending work gets cleaned up
|
||||
// but we don't clear the flag then. It is safe to reset it now.
|
||||
current.pendingWorkPriority = NoWork;
|
||||
}
|
||||
}
|
||||
current.sibling.return = current.return;
|
||||
current = current.sibling;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -70,6 +70,8 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) : Reconci
|
||||
const root = createFiberRoot(containerInfo);
|
||||
const container = root.current;
|
||||
// TODO: Use pending work/state instead of props.
|
||||
// TODO: This should not override the pendingWorkPriority if there is
|
||||
// higher priority work in the subtree.
|
||||
container.pendingProps = element;
|
||||
container.pendingWorkPriority = LowPriority;
|
||||
|
||||
|
||||
@@ -22,13 +22,9 @@ var ReactFiberCompleteWork = require('ReactFiberCompleteWork');
|
||||
var ReactFiberCommitWork = require('ReactFiberCommitWork');
|
||||
|
||||
var { cloneFiber } = require('ReactFiber');
|
||||
var { findNextUnitOfWorkAtPriority } = require('ReactFiberPendingWork');
|
||||
|
||||
var {
|
||||
NoWork,
|
||||
HighPriority,
|
||||
LowPriority,
|
||||
OffscreenPriority,
|
||||
} = require('ReactPriorityLevel');
|
||||
|
||||
var timeHeuristicForUnitOfWork = 1;
|
||||
@@ -65,33 +61,23 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
|
||||
// TODO: This is scanning one root at a time. It should be scanning all
|
||||
// roots for high priority work before moving on to lower priorities.
|
||||
let root = nextScheduledRoot;
|
||||
let highestPriorityRoot = null;
|
||||
let highestPriorityLevel = NoWork;
|
||||
while (root) {
|
||||
cloneFiber(root.current, root.current.pendingWorkPriority);
|
||||
// Find the highest possible priority work to do.
|
||||
// This loop is unrolled just to satisfy Flow's enum constraint.
|
||||
// We could make arbitrary many idle priority levels but having
|
||||
// too many just means flushing changes too often.
|
||||
let work = findNextUnitOfWorkAtPriority(root.current, HighPriority);
|
||||
if (work) {
|
||||
nextPriorityLevel = HighPriority;
|
||||
return work;
|
||||
}
|
||||
work = findNextUnitOfWorkAtPriority(root.current, LowPriority);
|
||||
if (work) {
|
||||
nextPriorityLevel = LowPriority;
|
||||
return work;
|
||||
}
|
||||
work = findNextUnitOfWorkAtPriority(root.current, OffscreenPriority);
|
||||
if (work) {
|
||||
nextPriorityLevel = OffscreenPriority;
|
||||
return work;
|
||||
if (highestPriorityLevel === NoWork ||
|
||||
highestPriorityLevel > root.current.pendingWorkPriority) {
|
||||
highestPriorityLevel = root.current.pendingWorkPriority;
|
||||
highestPriorityRoot = root;
|
||||
}
|
||||
// We didn't find anything to do in this root, so let's try the next one.
|
||||
root = root.nextScheduledRoot;
|
||||
}
|
||||
root = nextScheduledRoot;
|
||||
while (root) {
|
||||
root = root.nextScheduledRoot;
|
||||
if (highestPriorityRoot) {
|
||||
nextPriorityLevel = highestPriorityLevel;
|
||||
return cloneFiber(
|
||||
highestPriorityRoot.current,
|
||||
highestPriorityLevel
|
||||
);
|
||||
}
|
||||
|
||||
nextPriorityLevel = NoWork;
|
||||
@@ -103,7 +89,8 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
|
||||
// TODO: Error handling.
|
||||
let effectfulFiber = finishedWork.firstEffect;
|
||||
while (effectfulFiber) {
|
||||
commitWork(effectfulFiber);
|
||||
const current = effectfulFiber.alternate;
|
||||
commitWork(current, effectfulFiber);
|
||||
const next = effectfulFiber.nextEffect;
|
||||
// Ensure that we clean these up so that we don't accidentally keep them.
|
||||
// I'm not actually sure this matters because we can't reset firstEffect
|
||||
@@ -114,6 +101,24 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
|
||||
}
|
||||
}
|
||||
|
||||
function resetWorkPriority(workInProgress : Fiber) {
|
||||
let newPriority = NoWork;
|
||||
// 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) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
function completeUnitOfWork(workInProgress : Fiber) : ?Fiber {
|
||||
while (true) {
|
||||
// The current, flushed, state of this fiber is the alternate.
|
||||
@@ -123,6 +128,8 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
|
||||
const current = workInProgress.alternate;
|
||||
const next = completeWork(current, workInProgress);
|
||||
|
||||
resetWorkPriority(workInProgress);
|
||||
|
||||
// The work is now done. We don't need this anymore. This flags
|
||||
// to the system not to redo any work here.
|
||||
workInProgress.pendingProps = null;
|
||||
@@ -130,12 +137,6 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
|
||||
const returnFiber = workInProgress.return;
|
||||
|
||||
if (returnFiber) {
|
||||
// Ensure that remaining work priority bubbles up.
|
||||
if (workInProgress.pendingWorkPriority !== NoWork &&
|
||||
(returnFiber.pendingWorkPriority === NoWork ||
|
||||
returnFiber.pendingWorkPriority > workInProgress.pendingWorkPriority)) {
|
||||
returnFiber.pendingWorkPriority = workInProgress.pendingWorkPriority;
|
||||
}
|
||||
// Ensure that the first and last effect of the parent corresponds
|
||||
// to the children's first and last effect. This probably relies on
|
||||
// children completing in order.
|
||||
@@ -159,18 +160,16 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
|
||||
} else if (returnFiber) {
|
||||
// If there's no more work in this returnFiber. Complete the returnFiber.
|
||||
workInProgress = returnFiber;
|
||||
// If we're stepping up through the child, that means we can now commit
|
||||
// this work. We should only do this when we're stepping upwards because
|
||||
// completing a downprioritized item is not the same as completing its
|
||||
// children.
|
||||
if (workInProgress.childInProgress) {
|
||||
workInProgress.child = workInProgress.childInProgress;
|
||||
workInProgress.childInProgress = null;
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
// If we're at the root, there's no more work to do. We can flush it.
|
||||
const root : FiberRoot = (workInProgress.stateNode : any);
|
||||
if (root.current === workInProgress) {
|
||||
throw new Error(
|
||||
'Cannot commit the same tree as before. This is probably a bug ' +
|
||||
'related to the return field.'
|
||||
);
|
||||
}
|
||||
root.current = workInProgress;
|
||||
// TODO: We can be smarter here and only look for more work in the
|
||||
// "next" scheduled work since we've already scanned passed. That
|
||||
@@ -191,16 +190,13 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
|
||||
}
|
||||
|
||||
function performUnitOfWork(workInProgress : Fiber) : ?Fiber {
|
||||
// Ignore work if there is nothing to do.
|
||||
if (workInProgress.pendingProps === null) {
|
||||
return completeUnitOfWork(workInProgress);
|
||||
}
|
||||
// The current, flushed, state of this fiber is the alternate.
|
||||
// Ideally nothing should rely on this, but relying on it here
|
||||
// means that we don't need an additional field on the work in
|
||||
// progress.
|
||||
const current = workInProgress.alternate;
|
||||
const next = beginWork(current, workInProgress);
|
||||
const next = beginWork(current, workInProgress, nextPriorityLevel);
|
||||
|
||||
if (next) {
|
||||
// If this spawns new work, do that next.
|
||||
return next;
|
||||
|
||||
@@ -410,7 +410,7 @@ describe('ReactIncremental', () => {
|
||||
|
||||
// Init
|
||||
ReactNoop.render(<Foo text="foo" text2="foo" step={0} />);
|
||||
ReactNoop.flushLowPri(55);
|
||||
ReactNoop.flushLowPri(55 + 25);
|
||||
|
||||
// We only finish the higher priority work. So the low pri content
|
||||
// has not yet finished mounting.
|
||||
@@ -432,7 +432,7 @@ describe('ReactIncremental', () => {
|
||||
// Make a quick update which will schedule low priority work to
|
||||
// update the middle content.
|
||||
ReactNoop.render(<Foo text="bar" text2="bar" step={1} />);
|
||||
ReactNoop.flushLowPri(30);
|
||||
ReactNoop.flushLowPri(30 + 25);
|
||||
|
||||
expect(ops).toEqual(['Foo', 'Bar']);
|
||||
|
||||
@@ -526,7 +526,7 @@ describe('ReactIncremental', () => {
|
||||
ops = [];
|
||||
|
||||
// The middle content is now pending rendering...
|
||||
ReactNoop.flushLowPri(30);
|
||||
ReactNoop.flushLowPri(30 + 25);
|
||||
expect(ops).toEqual(['Content', 'Middle', 'Bar']); // One more Middle left.
|
||||
|
||||
ops = [];
|
||||
|
||||
@@ -177,7 +177,7 @@ describe('ReactIncrementalSideEffects', () => {
|
||||
// render some higher priority work. The middle content will bailout so
|
||||
// it remains untouched which means that it should reuse it next time.
|
||||
ReactNoop.render(<Foo text="foo" step={1} />);
|
||||
ReactNoop.flush(30);
|
||||
ReactNoop.flush();
|
||||
|
||||
// Since we did nothing to the middle subtree during the interuption,
|
||||
// we should be able to reuse the reconciliation work that we already did
|
||||
@@ -270,6 +270,86 @@ describe('ReactIncrementalSideEffects', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('can defer side-effects and resume them later on', function() {
|
||||
class Bar extends React.Component {
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return this.props.idx !== nextProps;
|
||||
}
|
||||
render() {
|
||||
return <span prop={this.props.idx} />;
|
||||
}
|
||||
}
|
||||
function Foo(props) {
|
||||
return (
|
||||
<div>
|
||||
<span prop={props.tick} />
|
||||
<div hidden={true}>
|
||||
<Bar idx={props.idx} />
|
||||
<Bar idx={props.idx + 1} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ReactNoop.render(<Foo tick={0} idx={0} />);
|
||||
ReactNoop.flushLowPri(40 + 25);
|
||||
expect(ReactNoop.root.children).toEqual([
|
||||
div(
|
||||
span(0),
|
||||
div(/*the spans are down-prioritized and not rendered yet*/)
|
||||
),
|
||||
]);
|
||||
ReactNoop.render(<Foo tick={1} idx={0} />);
|
||||
ReactNoop.flushLowPri(35 + 25);
|
||||
expect(ReactNoop.root.children).toEqual([
|
||||
div(
|
||||
span(1),
|
||||
div(/*still not rendered yet*/)
|
||||
),
|
||||
]);
|
||||
ReactNoop.flushLowPri(30 + 25);
|
||||
expect(ReactNoop.root.children).toEqual([
|
||||
div(
|
||||
span(1),
|
||||
div(
|
||||
// Now we had enough time to finish the spans.
|
||||
span(0),
|
||||
span(1)
|
||||
)
|
||||
),
|
||||
]);
|
||||
var innerSpanA = ReactNoop.root.children[0].children[1].children[1];
|
||||
ReactNoop.render(<Foo tick={2} idx={1} />);
|
||||
ReactNoop.flushLowPri(30 + 25);
|
||||
expect(ReactNoop.root.children).toEqual([
|
||||
div(
|
||||
span(2),
|
||||
div(
|
||||
// Still same old numbers.
|
||||
span(0),
|
||||
span(1)
|
||||
)
|
||||
),
|
||||
]);
|
||||
ReactNoop.flushLowPri(30);
|
||||
expect(ReactNoop.root.children).toEqual([
|
||||
div(
|
||||
span(2),
|
||||
div(
|
||||
// New numbers.
|
||||
span(1),
|
||||
span(2)
|
||||
)
|
||||
),
|
||||
]);
|
||||
|
||||
var innerSpanB = ReactNoop.root.children[0].children[1].children[1];
|
||||
// This should have been an update to an existing instance, not recreation.
|
||||
// We verify that by ensuring that the child instance was the same as
|
||||
// before.
|
||||
expect(innerSpanA).toBe(innerSpanB);
|
||||
});
|
||||
|
||||
|
||||
// TODO: Test that side-effects are not cut off when a work in progress node
|
||||
// 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?
|
||||
|
||||
Reference in New Issue
Block a user