Skip to content

Commit

Permalink
Simple updates using alternate fibers
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sebmarkbage committed Jun 7, 2016
1 parent eb705d1 commit cce58ff
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 16 deletions.
37 changes: 34 additions & 3 deletions src/renderers/shared/fiber/ReactChildFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,26 @@ 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;
}

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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
80 changes: 69 additions & 11 deletions src/renderers/shared/fiber/ReactFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,59 +25,90 @@ 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.

// 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,
child: null,
sibling: null,

key: key,

type: null,

stateNode: null,

// Fiber

child: null,
sibling: null,

ref: null,

input: null,
memoizedInput: null,
output: null,

hasPendingChanges: true,

stateNode: null,
alternate: null,

};
};
Expand All @@ -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;
Expand Down
16 changes: 15 additions & 1 deletion src/renderers/shared/fiber/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions src/renderers/shared/fiber/ReactFiberCompleteWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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.
Expand Down
17 changes: 16 additions & 1 deletion src/renderers/shared/fiber/ReactFiberReconciler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {};
},
Expand Down
49 changes: 49 additions & 0 deletions src/renderers/shared/fiber/__tests__/ReactIncremental-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']);

});

});

0 comments on commit cce58ff

Please sign in to comment.