Move child updates to use the reconciled effects

Instead of passing the full list of children every time to
update the host environment, we'll only do inserts/deletes.

We loop over all the placement effects first and then later
we do the rest.
This commit is contained in:
Sebastian Markbage 2016-10-05 02:20:37 -07:00 committed by Sebastian Markbåge
parent 442ab71fc7
commit 5de2821372
9 changed files with 258 additions and 36 deletions

View File

@ -46,6 +46,7 @@ function recursivelyAppendChildren(parent : Element, child : HostChildren<Instan
var DOMRenderer = ReactFiberReconciler({
updateContainer(container : Container, children : HostChildren<Instance | TextInstance>) : void {
// TODO: Containers should update similarly to other parents.
container.innerHTML = '';
recursivelyAppendChildren(container, children);
},
@ -63,25 +64,18 @@ var DOMRenderer = ReactFiberReconciler({
prepareUpdate(
domElement : Instance,
oldProps : Props,
newProps : Props,
children : HostChildren<Instance | TextInstance>
newProps : Props
) : boolean {
return true;
},
commitUpdate(domElement : Instance, oldProps : Props, newProps : Props, children : HostChildren<Instance | TextInstance>) : void {
domElement.innerHTML = '';
recursivelyAppendChildren(domElement, children);
commitUpdate(domElement : Instance, oldProps : Props, newProps : Props) : void {
if (typeof newProps.children === 'string' ||
typeof newProps.children === 'number') {
domElement.textContent = newProps.children;
}
},
deleteInstance(instance : Instance) : void {
// Noop
},
createTextInstance(text : string) : TextInstance {
return document.createTextNode(text);
},
@ -90,6 +84,18 @@ var DOMRenderer = ReactFiberReconciler({
textInstance.nodeValue = newText;
},
appendChild(parentInstance : Instance, child : Instance | TextInstance) : void {
parentInstance.appendChild(child);
},
insertBefore(parentInstance : Instance, child : Instance | TextInstance, beforeChild : Instance | TextInstance) : void {
parentInstance.insertBefore(child, beforeChild);
},
removeChild(parentInstance : Instance, child : Instance | TextInstance) : void {
parentInstance.removeChild(child);
},
scheduleAnimationCallback: window.requestAnimationFrame,
scheduleDeferredCallback: window.requestIdleCallback,

View File

@ -81,18 +81,14 @@ var NoopRenderer = ReactFiberReconciler({
return inst;
},
prepareUpdate(instance : Instance, oldProps : Props, newProps : Props, children : HostChildren<Instance | TextInstance>) : boolean {
prepareUpdate(instance : Instance, oldProps : Props, newProps : Props) : boolean {
return true;
},
commitUpdate(instance : Instance, oldProps : Props, newProps : Props, children : HostChildren<Instance | TextInstance>) : void {
instance.children = flattenChildren(children);
commitUpdate(instance : Instance, oldProps : Props, newProps : Props) : void {
instance.prop = newProps.prop;
},
deleteInstance(instance : Instance) : void {
},
createTextInstance(text : string) : TextInstance {
var inst = { tag: TEXT_TAG, text : text };
// Hide from unit tests
@ -104,6 +100,34 @@ var NoopRenderer = ReactFiberReconciler({
textInstance.text = newText;
},
appendChild(parentInstance : Instance, child : Instance | TextInstance) : void {
const index = parentInstance.children.indexOf(child);
if (index !== -1) {
parentInstance.children.splice(index, 1);
}
parentInstance.children.push(child);
},
insertBefore(parentInstance : Instance, child : Instance | TextInstance, beforeChild : Instance | TextInstance) : void {
const index = parentInstance.children.indexOf(child);
if (index !== -1) {
parentInstance.children.splice(index, 1);
}
const beforeIndex = parentInstance.children.indexOf(beforeChild);
if (beforeIndex === -1) {
throw new Error('This child does not exist.');
}
parentInstance.children.splice(beforeIndex, 0, child);
},
removeChild(parentInstance : Instance, child : Instance | TextInstance) : void {
const index = parentInstance.children.indexOf(child);
if (index === -1) {
throw new Error('This child does not exist.');
}
parentInstance.children.splice(index, 1);
},
scheduleAnimationCallback(callback) {
scheduledAnimationCallback = callback;
},

View File

@ -183,6 +183,15 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
}
}
function placeSingleChild(newFiber : Fiber) {
// This is simpler for the single child case. We only need to do a
// placement for inserting new children.
if (shouldTrackSideEffects && !newFiber.alternate) {
newFiber.effectTag = Placement;
}
return newFiber;
}
function updateTextNode(
returnFiber : Fiber,
current : ?Fiber,
@ -738,39 +747,39 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
// fragment nodes. Recursion happens at the normal flow.
if (typeof newChild === 'string' || typeof newChild === 'number') {
return reconcileSingleTextNode(
return placeSingleChild(reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild,
priority
);
));
}
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return reconcileSingleElement(
return placeSingleChild(reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
priority
);
));
case REACT_COROUTINE_TYPE:
return reconcileSingleCoroutine(
return placeSingleChild(reconcileSingleCoroutine(
returnFiber,
currentFirstChild,
newChild,
priority
);
));
case REACT_YIELD_TYPE:
return reconcileSingleYield(
return placeSingleChild(reconcileSingleYield(
returnFiber,
currentFirstChild,
newChild,
priority
);
));
}
if (isArray(newChild)) {

View File

@ -50,6 +50,9 @@ var {
addCallbackToQueue,
mergeUpdateQueue,
} = require('ReactFiberUpdateQueue');
var {
Placement,
} = require('ReactTypeOfSideEffect');
var ReactInstanceMap = require('ReactInstanceMap');
module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>, getScheduler : () => Scheduler) {
@ -299,6 +302,20 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>, g
// Reconcile the children and stash them for later work.
reconcileChildrenAtPriority(current, workInProgress, nextChildren, OffscreenPriority);
workInProgress.child = current ? current.child : null;
if (!current) {
// If this doesn't have a current we won't track it for placement
// effects. However, when we come back around to this we have already
// inserted the parent which means that we'll infact need to make this a
// placement.
// TODO: There has to be a better solution to this problem.
let child = workInProgress.progressedChild;
while (child) {
child.effectTag = Placement;
child = child.sibling;
}
}
// Abort and don't process children yet.
return null;
} else {

View File

@ -25,12 +25,146 @@ var {
} = ReactTypeOfWork;
var { callCallbacks } = require('ReactFiberUpdateQueue');
var {
Placement,
PlacementAndUpdate,
} = require('ReactTypeOfSideEffect');
module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
const updateContainer = config.updateContainer;
const commitUpdate = config.commitUpdate;
const commitTextUpdate = config.commitTextUpdate;
const appendChild = config.appendChild;
const insertBefore = config.insertBefore;
const removeChild = config.removeChild;
function getHostParent(fiber : Fiber) : ?I {
let parent = fiber.return;
while (parent) {
switch (parent.tag) {
case HostComponent:
return parent.stateNode;
case HostContainer:
// TODO: Currently we use the updateContainer feature to update these,
// but we should be able to handle this case too.
return null;
}
parent = parent.return;
}
return null;
}
function getHostSibling(fiber : Fiber) : ?I {
// We're going to search forward into the tree until we find a sibling host
// node. Unfortunately, if multiple insertions are done in a row we have to
// search past them. This leads to exponential search for the next sibling.
// TODO: Find a more efficient way to do this.
let node : Fiber = fiber;
siblings: while (true) {
// If we didn't find anything, let's try the next sibling.
while (!node.sibling) {
if (!node.return || node.return.tag === HostComponent) {
// If we pop out of the root or hit the parent the fiber we are the
// last sibling.
return null;
}
node = node.return;
}
node = node.sibling;
while (node.tag !== HostComponent && node.tag !== HostText) {
// If it is not host node and, we might have a host node inside it.
// Try to search down until we find one.
// TODO: For coroutines, this will have to search the stateNode.
if (node.effectTag === Placement ||
node.effectTag === PlacementAndUpdate) {
// If we don't have a child, try the siblings instead.
continue siblings;
}
if (!node.child) {
continue siblings;
} else {
node = node.child;
}
}
// Check if this host node is stable or about to be placed.
if (node.effectTag !== Placement &&
node.effectTag !== PlacementAndUpdate) {
// Found it!
return node.stateNode;
}
}
}
function commitInsertion(finishedWork : Fiber) : void {
// Recursively insert all host nodes into the parent.
const parent = getHostParent(finishedWork);
if (!parent) {
return;
}
const before = getHostSibling(finishedWork);
// We only have the top Fiber that was inserted but we need recurse down its
// children to find all the terminal nodes.
let node : Fiber = finishedWork;
while (true) {
if (node.tag === HostComponent || node.tag === HostText) {
if (before) {
insertBefore(parent, node.stateNode, before);
} else {
appendChild(parent, node.stateNode);
}
} else if (node.child) {
// TODO: Coroutines need to visit the stateNode.
node = node.child;
continue;
}
if (node === finishedWork) {
return;
}
while (!node.sibling) {
if (!node.return || node.return === finishedWork) {
return;
}
node = node.return;
}
node = node.sibling;
}
}
function commitDeletion(current : Fiber) : void {
// Recursively delete all host nodes from the parent.
const parent = getHostParent(current);
if (!parent) {
return;
}
// We only have the top Fiber that was inserted but we need recurse down its
// children to find all the terminal nodes.
// TODO: Call componentWillUnmount on all classes as needed. Recurse down
// removed HostComponents but don't call removeChild on already removed
// children.
let node : Fiber = current;
while (true) {
if (node.tag === HostComponent || node.tag === HostText) {
removeChild(parent, node.stateNode);
} else if (node.child) {
// TODO: Coroutines need to visit the stateNode.
node = node.child;
continue;
}
if (node === current) {
return;
}
while (!node.sibling) {
if (!node.return || node.return === current) {
return;
}
node = node.return;
}
node = node.sibling;
}
}
function commitWork(current : ?Fiber, finishedWork : Fiber) : void {
switch (finishedWork.tag) {
case ClassComponent: {
@ -62,12 +196,10 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
throw new Error('This should only be done during updates.');
}
// Commit the work prepared earlier.
const child = finishedWork.child;
const children = (child && !child.sibling) ? (child.output : ?Fiber | I) : child;
const newProps = finishedWork.memoizedProps;
const oldProps = current.memoizedProps;
const instance : I = finishedWork.stateNode;
commitUpdate(instance, oldProps, newProps, children);
commitUpdate(instance, oldProps, newProps);
return;
}
case HostText: {
@ -86,6 +218,8 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
return {
commitInsertion,
commitDeletion,
commitWork,
};

View File

@ -142,8 +142,6 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
return null;
case HostComponent:
let newProps = workInProgress.pendingProps;
const child = workInProgress.child;
const children = (child && !child.sibling) ? (child.output : ?Fiber | I) : child;
if (current && workInProgress.stateNode != null) {
// If we have an alternate, that means this is an update and we need to
// schedule a side-effect to do the updates.
@ -156,7 +154,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
newProps = oldProps;
}
const instance : I = workInProgress.stateNode;
if (prepareUpdate(instance, oldProps, newProps, children)) {
if (prepareUpdate(instance, oldProps, newProps)) {
// This returns true if there was something to update.
markUpdate(workInProgress);
}
@ -171,6 +169,8 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
return null;
}
}
const child = workInProgress.child;
const children = (child && !child.sibling) ? (child.output : ?Fiber | I) : child;
const instance = createInstance(workInProgress.type, newProps, children);
// TODO: This seems like unnecessary duplication.
workInProgress.stateNode = instance;

View File

@ -34,16 +34,19 @@ export type HostConfig<T, P, I, TI, C> = {
// reorder so we host will always need to check the set. We should make a flag
// or something so that it can bailout easily.
updateContainer(containerInfo : C, children : HostChildren<I | TI>) : void;
updateContainer(containerInfo : C, children : HostChildren<I | TI>) : void,
createInstance(type : T, props : P, children : HostChildren<I | TI>) : I,
prepareUpdate(instance : I, oldProps : P, newProps : P, children : HostChildren<I | TI>) : boolean,
commitUpdate(instance : I, oldProps : P, newProps : P, children : HostChildren<I | TI>) : void,
deleteInstance(instance : I) : void,
prepareUpdate(instance : I, oldProps : P, newProps : P) : boolean,
commitUpdate(instance : I, oldProps : P, newProps : P) : void,
createTextInstance(text : string) : TI,
commitTextUpdate(textInstance : TI, oldText : string, newText : string) : void,
appendChild(parentInstance : I, child : I | TI) : void,
insertBefore(parentInstance : I, child : I | TI, beforeChild : I | TI) : void,
removeChild(parentInstance : I, child : I | TI) : void,
scheduleAnimationCallback(callback : () => void) : void,
scheduleDeferredCallback(callback : (deadline : Deadline) => void) : void

View File

@ -32,8 +32,10 @@ var {
var {
NoEffect,
Placement,
Update,
PlacementAndUpdate,
Deletion,
} = require('ReactTypeOfSideEffect');
var timeHeuristicForUnitOfWork = 1;
@ -52,7 +54,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
const { beginWork } = ReactFiberBeginWork(config, getScheduler);
const { completeWork } = ReactFiberCompleteWork(config);
const { commitWork } = ReactFiberCommitWork(config);
const { commitInsertion, commitDeletion, commitWork } = ReactFiberCommitWork(config);
const scheduleAnimationCallback = config.scheduleAnimationCallback;
const scheduleDeferredCallback = config.scheduleDeferredCallback;
@ -109,7 +111,24 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
function commitAllWork(finishedWork : Fiber) {
// Commit all the side-effects within a tree.
// TODO: Error handling.
// First, we'll perform all the host insertion and deletion effects.
let effectfulFiber = finishedWork.firstEffect;
while (effectfulFiber) {
switch (effectfulFiber.effectTag) {
case Placement:
case PlacementAndUpdate:
commitInsertion(effectfulFiber);
break;
case Deletion:
commitDeletion(effectfulFiber);
break;
}
effectfulFiber = effectfulFiber.nextEffect;
}
// Next, we'll perform all other effects.
effectfulFiber = finishedWork.firstEffect;
while (effectfulFiber) {
const current = effectfulFiber.alternate;
if (effectfulFiber.effectTag === Update ||

View File

@ -71,7 +71,11 @@ describe('ReactIncrementalSideEffects', () => {
{props.text === 'World' ? [
<Bar key="a" text={props.text} />,
<div key="b" />,
] : props.text === 'Hi' ? [
<div key="b" />,
<Bar key="a" text={props.text} />,
] : null}
<span prop="test" />
</div>
);
}
@ -79,13 +83,19 @@ describe('ReactIncrementalSideEffects', () => {
ReactNoop.render(<Foo text="Hello" />);
ReactNoop.flush();
expect(ReactNoop.root.children).toEqual([
div(span()),
div(span(), span('test')),
]);
ReactNoop.render(<Foo text="World" />);
ReactNoop.flush();
expect(ReactNoop.root.children).toEqual([
div(span(), span(), div()),
div(span(), span(), div(), span('test')),
]);
ReactNoop.render(<Foo text="Hi" />);
ReactNoop.flush();
expect(ReactNoop.root.children).toEqual([
div(span(), div(), span(), span('test')),
]);
});