mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
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:
committed by
Paul O’Shannessy
parent
07389fd9fc
commit
7912baea69
@@ -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;
|
||||
|
||||
@@ -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']);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user