mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
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:
@@ -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;
|
||||
|
||||
@@ -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']);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user