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:
parent
442ab71fc7
commit
5de2821372
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 ||
|
||||
|
|
|
@ -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')),
|
||||
]);
|
||||
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue