Merge pull request #6981 from sebmarkbage/newreconciler

[Fiber] Add support for simple updates and fiber pooling
(cherry picked from commit ccd26ee020)
This commit is contained in:
Sebastian Markbåge
2016-06-14 15:50:38 -07:00
committed by Paul O’Shannessy
parent 07389fd9fc
commit 7912baea69
6 changed files with 254 additions and 84 deletions
+34 -3
View File
@@ -28,7 +28,7 @@ var {
var ReactFiber = require('ReactFiber');
var ReactReifiedYield = require('ReactReifiedYield');
function createSubsequentChild(parent : Fiber, previousSibling : Fiber, newChildren) : Fiber {
function createSubsequentChild(parent : Fiber, nextReusable : ?Fiber, previousSibling : Fiber, newChildren) : Fiber {
if (typeof newChildren !== 'object' || newChildren === null) {
return previousSibling;
}
@@ -36,6 +36,18 @@ function createSubsequentChild(parent : Fiber, previousSibling : Fiber, newChild
switch (newChildren.$$typeof) {
case REACT_ELEMENT_TYPE: {
const element = (newChildren : ReactElement<any>);
if (nextReusable &&
element.type === nextReusable.type &&
element.key === nextReusable.key) {
// TODO: This is not sufficient since previous siblings could be new.
// Will fix reconciliation properly later.
const clone = ReactFiber.cloneFiber(nextReusable);
clone.input = element.props;
clone.child = nextReusable.child;
clone.sibling = null;
previousSibling.sibling = clone;
return clone;
}
const child = ReactFiber.createFiberFromElement(element);
previousSibling.sibling = child;
child.parent = parent;
@@ -64,7 +76,11 @@ function createSubsequentChild(parent : Fiber, previousSibling : Fiber, newChild
if (Array.isArray(newChildren)) {
let prev : Fiber = previousSibling;
for (var i = 0; i < newChildren.length; i++) {
prev = createSubsequentChild(parent, prev, newChildren[i]);
let reusable = null;
if (prev.alternate) {
reusable = prev.alternate.sibling;
}
prev = createSubsequentChild(parent, reusable, prev, newChildren[i]);
}
return prev;
} else {
@@ -81,6 +97,17 @@ function createFirstChild(parent, newChildren) {
switch (newChildren.$$typeof) {
case REACT_ELEMENT_TYPE: {
const element = (newChildren : ReactElement<any>);
const existingChild : ?Fiber = parent.child;
if (existingChild &&
element.type === existingChild.type &&
element.key === existingChild.key) {
// Get the clone of the existing fiber.
const clone = ReactFiber.cloneFiber(existingChild);
clone.input = element.props;
clone.child = existingChild.child;
clone.sibling = null;
return clone;
}
const child = ReactFiber.createFiberFromElement(element);
child.parent = parent;
return child;
@@ -114,7 +141,11 @@ function createFirstChild(parent, newChildren) {
prev = createFirstChild(parent, newChildren[i]);
first = prev;
} else {
prev = createSubsequentChild(parent, prev, newChildren[i]);
let reusable = null;
if (prev.alternate) {
reusable = prev.alternate.sibling;
}
prev = createSubsequentChild(parent, reusable, prev, newChildren[i]);
}
}
return first;
+69 -11
View File
@@ -25,28 +25,47 @@ var ReactElement = require('ReactElement');
import type { ReactCoroutine, ReactYield } from 'ReactCoroutine';
export type Fiber = {
// An Instance is shared between all versions of a component. We can easily
// break this out into a separate object to avoid copying so much to the
// alternate versions of the tree. We put this on a single object for now to
// minimize the number of objects created during the initial render.
type Instance = {
// Tag identifying the type of fiber.
tag: number,
// Singly Linked List Tree Structure.
parent: ?Fiber, // Consider a regenerated temporary parent stack instead.
child: ?Fiber,
sibling: ?Fiber,
// The parent Fiber used to create this one. The type is constrained to the
// Instance part of the Fiber since it is not safe to traverse the tree from
// the instance.
parent: ?Instance, // Consider a regenerated temporary parent stack instead.
// Unique identifier of this child.
key: ?string,
key: null | string,
// The function/class/module associated with this fiber.
type: any,
// The local state associated with this fiber.
stateNode: ?Object,
};
// A Fiber is work on a Component that needs to be done or was done. There can
// be more than one per component.
export type Fiber = Instance & {
// Singly Linked List Tree Structure.
child: ?Fiber,
sibling: ?Fiber,
// The ref last used to attach this node.
// I'll avoid adding an owner field for prod and model that as functions.
ref: null | (handle : ?Object) => void,
// Input is the data coming into process this fiber. Arguments. Props.
input: any, // This type will be more specific once we overload the tag.
// TODO: I think that there is a way to merge input and memoizedInput somehow.
memoizedInput: any, // The input used to create the output.
// Output is the return value of this fiber, or a linked list of return values
// if this returns multiple values. Such as a fragment.
output: any, // This type will be more specific once we overload the tag.
@@ -54,30 +73,42 @@ export type Fiber = {
// This will be used to quickly determine if a subtree has no pending changes.
hasPendingChanges: bool,
// The local state associated with this fiber.
stateNode: ?Object,
// 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,
};
var createFiber = function(tag : number, key : null | string) : Fiber {
return {
// Instance
tag: tag,
parent: null,
key: key,
type: null,
stateNode: null,
// Fiber
child: null,
sibling: null,
key: key,
type: null,
ref: null,
input: null,
memoizedInput: null,
output: null,
hasPendingChanges: true,
stateNode: null,
alternate: null,
};
};
@@ -86,6 +117,33 @@ function shouldConstruct(Component) {
return !!(Component.prototype && Component.prototype.isReactComponent);
}
// This is used to create an alternate fiber to do work on.
exports.cloneFiber = function(fiber : Fiber) : 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.
if (fiber.alternate) {
return fiber.alternate;
}
// This should not have an alternate already
var alt = createFiber(fiber.tag, fiber.key);
if (fiber.parent) {
// TODO: This assumes the parent's alternate is already created.
// Stop using the alternates of parents once we have a parent stack.
// $FlowFixMe: This downcast is not safe. It is intentionally an error.
alt.parent = fiber.parent.alternate;
}
alt.type = fiber.type;
alt.stateNode = fiber.stateNode;
alt.alternate = fiber;
fiber.alternate = alt;
return alt;
};
exports.createFiberFromElement = function(element : ReactElement) {
const fiber = exports.createFiberFromElementType(element.type, element.key);
fiber.input = element.props;
@@ -27,100 +27,114 @@ var {
YieldComponent,
} = ReactTypesOfWork;
function updateFunctionalComponent(unitOfWork) {
var fn = unitOfWork.type;
var props = unitOfWork.input;
console.log('perform work on:', fn.name);
function updateFunctionalComponent(workInProgress) {
var fn = workInProgress.type;
var props = workInProgress.input;
console.log('update fn:', fn.name);
var nextChildren = fn(props);
unitOfWork.child = ReactChildFiber.reconcileChildFibers(
unitOfWork,
unitOfWork.child,
workInProgress.child = ReactChildFiber.reconcileChildFibers(
workInProgress,
workInProgress.child,
nextChildren
);
}
function updateHostComponent(unitOfWork) {
console.log('host component', unitOfWork.type, typeof unitOfWork.input.children === 'string' ? unitOfWork.input.children : '');
function updateHostComponent(workInProgress) {
console.log('host component', workInProgress.type, typeof workInProgress.input.children === 'string' ? workInProgress.input.children : '');
var nextChildren = unitOfWork.input.children;
unitOfWork.child = ReactChildFiber.reconcileChildFibers(
unitOfWork,
unitOfWork.child,
var nextChildren = workInProgress.input.children;
workInProgress.child = ReactChildFiber.reconcileChildFibers(
workInProgress,
workInProgress.child,
nextChildren
);
}
function mountIndeterminateComponent(unitOfWork) {
var fn = unitOfWork.type;
var props = unitOfWork.input;
function mountIndeterminateComponent(workInProgress) {
var fn = workInProgress.type;
var props = workInProgress.input;
var value = fn(props);
if (typeof value === 'object' && value && typeof value.render === 'function') {
console.log('performed work on class:', fn.name);
// Proceed under the assumption that this is a class instance
unitOfWork.tag = ClassComponent;
workInProgress.tag = ClassComponent;
} else {
console.log('performed work on fn:', fn.name);
// Proceed under the assumption that this is a functional component
unitOfWork.tag = FunctionalComponent;
workInProgress.tag = FunctionalComponent;
}
unitOfWork.child = ReactChildFiber.reconcileChildFibers(
unitOfWork,
unitOfWork.child,
workInProgress.child = ReactChildFiber.reconcileChildFibers(
workInProgress,
workInProgress.child,
value
);
}
function updateCoroutineComponent(unitOfWork) {
var coroutine = (unitOfWork.input : ?ReactCoroutine);
function updateCoroutineComponent(workInProgress) {
var coroutine = (workInProgress.input : ?ReactCoroutine);
if (!coroutine) {
throw new Error('Should be resolved by now');
}
console.log('begin coroutine', unitOfWork.type.name);
unitOfWork.child = ReactChildFiber.reconcileChildFibers(
unitOfWork,
unitOfWork.child,
console.log('begin coroutine', workInProgress.type.name);
workInProgress.child = ReactChildFiber.reconcileChildFibers(
workInProgress,
workInProgress.child,
coroutine.children
);
}
function beginWork(unitOfWork : Fiber) : ?Fiber {
switch (unitOfWork.tag) {
function beginWork(workInProgress : Fiber) : ?Fiber {
const alt = workInProgress.alternate;
if (alt && workInProgress.input === alt.memoizedInput) {
// The most likely scenario is that the previous copy of the tree contains
// the same input as the new one. In that case, we can just copy the output
// and children from that node.
workInProgress.output = alt.output;
workInProgress.child = alt.child;
return null;
}
if (workInProgress.input === workInProgress.memoizedInput) {
// In a ping-pong scenario, this version could actually contain the
// old input. In that case, we can just bail out.
return null;
}
switch (workInProgress.tag) {
case IndeterminateComponent:
mountIndeterminateComponent(unitOfWork);
mountIndeterminateComponent(workInProgress);
break;
case FunctionalComponent:
updateFunctionalComponent(unitOfWork);
updateFunctionalComponent(workInProgress);
break;
case ClassComponent:
console.log('class component', unitOfWork.input.type.name);
console.log('class component', workInProgress.input.type.name);
break;
case HostComponent:
updateHostComponent(unitOfWork);
updateHostComponent(workInProgress);
break;
case CoroutineHandlerPhase:
// This is a restart. Reset the tag to the initial phase.
unitOfWork.tag = CoroutineComponent;
workInProgress.tag = CoroutineComponent;
// Intentionally fall through since this is now the same.
case CoroutineComponent:
updateCoroutineComponent(unitOfWork);
updateCoroutineComponent(workInProgress);
// This doesn't take arbitrary time so we could synchronously just begin
// eagerly do the work of unitOfWork.child as an optimization.
if (unitOfWork.child) {
return beginWork(unitOfWork.child);
// eagerly do the work of workInProgress.child as an optimization.
if (workInProgress.child) {
return beginWork(workInProgress.child);
}
break;
case YieldComponent:
// A yield component is just a placeholder, we can just run through the
// next one immediately.
if (unitOfWork.sibling) {
return beginWork(unitOfWork.sibling);
if (workInProgress.sibling) {
return beginWork(workInProgress.sibling);
}
return null;
default:
throw new Error('Unknown unit of work tag');
}
return unitOfWork.child;
return workInProgress.child;
}
exports.beginWork = beginWork;
@@ -34,6 +34,7 @@ function transferOutput(child : ?Fiber, parent : Fiber) {
// avoid unnecessary traversal. When we have multiple output, we just pass
// the linked list of fibers that has the individual output values.
parent.output = (child && !child.sibling) ? child.output : child;
parent.memoizedInput = parent.input;
}
function recursivelyFillYields(yields, output : ?Fiber | ?ReifiedYield) {
@@ -53,8 +54,8 @@ function recursivelyFillYields(yields, output : ?Fiber | ?ReifiedYield) {
}
}
function moveCoroutineToHandlerPhase(unitOfWork : Fiber) {
var coroutine = (unitOfWork.input : ?ReactCoroutine);
function moveCoroutineToHandlerPhase(workInProgress : Fiber) {
var coroutine = (workInProgress.input : ?ReactCoroutine);
if (!coroutine) {
throw new Error('Should be resolved by now');
}
@@ -64,12 +65,14 @@ function moveCoroutineToHandlerPhase(unitOfWork : Fiber) {
// single component, or at least tail call optimize nested ones. Currently
// that requires additional fields that we don't want to add to the fiber.
// So this requires nested handlers.
unitOfWork.tag = CoroutineHandlerPhase;
// Note: This doesn't mutate the alternate node. I don't think it needs to
// since this stage is reset for every pass.
workInProgress.tag = CoroutineHandlerPhase;
// Build up the yields.
// TODO: Compare this to a generator or opaque helpers like Children.
var yields : Array<ReifiedYield> = [];
var child = unitOfWork.child;
var child = workInProgress.child;
while (child) {
recursivelyFillYields(yields, child.output);
child = child.sibling;
@@ -78,34 +81,34 @@ function moveCoroutineToHandlerPhase(unitOfWork : Fiber) {
var props = coroutine.props;
var nextChildren = fn(props, yields);
unitOfWork.stateNode = ReactChildFiber.reconcileChildFibers(
unitOfWork,
unitOfWork.stateNode,
workInProgress.stateNode = ReactChildFiber.reconcileChildFibers(
workInProgress,
workInProgress.stateNode,
nextChildren
);
return unitOfWork.stateNode;
return workInProgress.stateNode;
}
exports.completeWork = function(unitOfWork : Fiber) : ?Fiber {
switch (unitOfWork.tag) {
exports.completeWork = function(workInProgress : Fiber) : ?Fiber {
switch (workInProgress.tag) {
case FunctionalComponent:
console.log('/functional component', unitOfWork.type.name);
transferOutput(unitOfWork.child, unitOfWork);
console.log('/functional component', workInProgress.type.name);
transferOutput(workInProgress.child, workInProgress);
break;
case ClassComponent:
console.log('/class component', unitOfWork.type.name);
transferOutput(unitOfWork.child, unitOfWork);
console.log('/class component', workInProgress.type.name);
transferOutput(workInProgress.child, workInProgress);
break;
case HostComponent:
console.log('/host component', unitOfWork.type);
console.log('/host component', workInProgress.type);
break;
case CoroutineComponent:
console.log('/coroutine component', unitOfWork.input.handler.name);
return moveCoroutineToHandlerPhase(unitOfWork);
console.log('/coroutine component', workInProgress.input.handler.name);
return moveCoroutineToHandlerPhase(workInProgress);
case CoroutineHandlerPhase:
transferOutput(unitOfWork.stateNode, unitOfWork);
transferOutput(workInProgress.stateNode, workInProgress);
// Reset the tag to now be a first phase coroutine.
unitOfWork.tag = CoroutineComponent;
workInProgress.tag = CoroutineComponent;
break;
case YieldComponent:
// Does nothing.
@@ -49,18 +49,23 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) : Reconciler {
let nextUnitOfWork : ?Fiber = null;
function completeUnitOfWork(unitOfWork : Fiber) : ?Fiber {
function completeUnitOfWork(workInProgress : Fiber) : ?Fiber {
while (true) {
var next = completeWork(unitOfWork);
var next = completeWork(workInProgress);
if (next) {
// If completing this work spawned new work, do that next.
return next;
} else if (unitOfWork.sibling) {
} else if (workInProgress.sibling) {
// If there is more work to do in this parent, do that next.
return unitOfWork.sibling;
} else if (unitOfWork.parent) {
return workInProgress.sibling;
} else if (workInProgress.parent) {
// If there's no more work in this parent. Complete the parent.
unitOfWork = unitOfWork.parent;
// TODO: Stop using the parent for this purpose. I think this will break
// down in edge cases because when nodes are reused during bailouts, we
// don't know which of two parents was used. Instead we should maintain
// a temporary manual stack.
// $FlowFixMe: This downcast is not safe. It is intentionally an error.
workInProgress = workInProgress.parent;
} else {
// If we're at the root, there's no more work to do.
return null;
@@ -68,14 +73,14 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) : Reconciler {
}
}
function performUnitOfWork(unitOfWork : Fiber) : ?Fiber {
var next = beginWork(unitOfWork);
function performUnitOfWork(workInProgress : Fiber) : ?Fiber {
var next = beginWork(workInProgress);
if (next) {
// If this spawns new work, do that next.
return next;
} else {
// Otherwise, complete the current work.
return completeUnitOfWork(unitOfWork);
return completeUnitOfWork(workInProgress);
}
}
@@ -107,13 +112,23 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) : Reconciler {
}
*/
let rootFiber : ?Fiber = null;
return {
mountNewRoot(element : ReactElement<any>) : OpaqueID {
ensureLowPriIsScheduled();
nextUnitOfWork = ReactFiber.createFiberFromElement(element);
// TODO: Unify this with ReactChildFiber. We can't now because the parent
// is passed. Should be doable though. Might require a wrapper don't know.
if (rootFiber && rootFiber.type === element.type && rootFiber.key === element.key) {
nextUnitOfWork = rootFiber;
rootFiber.input = element.props;
return {};
}
nextUnitOfWork = rootFiber = ReactFiber.createFiberFromElement(element);
return {};
},
@@ -66,4 +66,53 @@ describe('ReactIncremental', function() {
expect(barCalled).toBe(true);
});
it('updates a previous render', function() {
var ops = [];
function Header() {
ops.push('Header');
return <h1>Hi</h1>;
}
function Content(props) {
ops.push('Content');
return <div>{props.children}</div>;
}
function Footer() {
ops.push('Footer');
return <footer>Bye</footer>;
}
var header = <Header />;
var footer = <Footer />;
function Foo(props) {
ops.push('Foo');
return (
<div>
{header}
<Content>{props.text}</Content>
{footer}
</div>
);
}
ReactNoop.render(<Foo text="foo" />);
ReactNoop.flush();
expect(ops).toEqual(['Foo', 'Header', 'Content', 'Footer']);
ops = [];
ReactNoop.render(<Foo text="bar" />);
ReactNoop.flush();
// Since this is an update, it should bail out and reuse the work from
// Header and Content.
expect(ops).toEqual(['Foo', 'Content']);
});
});