Simple updates using alternate fibers

This splits the Fiber type into Fiber and Instance. This could be
two different object instances to save memory. However, to avoid
GC thrash I merge them into one.

When ReactChildFiber reconciles children, it clones the previous
fiber. This creates a new tree for work-in-progress. The idea is
that once flushed, this new tree will be used at the root.

However, we know that we'll never need more than two trees
at a time. Therefore my clone function stores the clone on the
original. Effectively this creates a fiber pool.

Ideally, the .alternate field shouldn't be used outside of clone
so that everything can work with pure immutability. I cheat a bit
for now so I don't have to pass both trees everywhere.

ReactChildFiber is a bit hacky for reuse and doesn't solve all
cases. Will fix that once I try to get parity.
This commit is contained in:
Sebastian Markbage
2016-06-06 22:00:51 -07:00
parent eb705d1448
commit cce58ffd62
6 changed files with 186 additions and 16 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;
@@ -30,7 +30,7 @@ var {
function updateFunctionalComponent(unitOfWork) {
var fn = unitOfWork.type;
var props = unitOfWork.input;
console.log('perform work on:', fn.name);
console.log('update fn:', fn.name);
var nextChildren = fn(props);
unitOfWork.child = ReactChildFiber.reconcileChildFibers(
@@ -85,6 +85,20 @@ function updateCoroutineComponent(unitOfWork) {
}
function beginWork(unitOfWork : Fiber) : ?Fiber {
const alt = unitOfWork.alternate;
if (alt && unitOfWork.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.
unitOfWork.output = alt.output;
unitOfWork.child = alt.child;
return null;
}
if (unitOfWork.input === unitOfWork.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 (unitOfWork.tag) {
case IndeterminateComponent:
mountIndeterminateComponent(unitOfWork);
@@ -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) {
@@ -64,6 +65,8 @@ 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.
// Note: This doesn't mutate the alternate node. I don't think it needs to
// since this stage is reset for every pass.
unitOfWork.tag = CoroutineHandlerPhase;
// Build up the yields.
@@ -60,6 +60,11 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) : Reconciler {
return unitOfWork.sibling;
} else if (unitOfWork.parent) {
// If there's no more work in this parent. Complete the 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.
unitOfWork = unitOfWork.parent;
} else {
// If we're at the root, there's no more work to do.
@@ -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']);
});
});