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:
Sebastian Markbåge
2016-10-03 17:58:48 -07:00
committed by Paul O’Shannessy
parent d5059c91f5
commit 3bc9d9caa7
11 changed files with 370 additions and 361 deletions
+6 -10
View File
@@ -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) {
+48 -2
View File
@@ -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;
}
};
+51 -15
View File
@@ -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;
+128 -154
View File
@@ -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?