Merge pull request #8400 from sebmarkbage/fibercontainerchildren

[Fiber] Update root children using appendChild/insertBefore/removeChild
This commit is contained in:
Sebastian Markbåge
2016-11-23 18:09:50 -08:00
committed by GitHub
8 changed files with 153 additions and 129 deletions
-1
View File
@@ -120,4 +120,3 @@ src/renderers/shared/stack/reconciler/__tests__/refs-test.js
src/test/__tests__/ReactTestUtils-test.js
* traverses children in the correct order
* should support injected wrapper components as DOM components
+1
View File
@@ -1560,6 +1560,7 @@ src/test/__tests__/ReactTestUtils-test.js
* can scryRenderedDOMComponentsWithClass with TextComponent
* can scryRenderedDOMComponentsWithClass with className contains \n
* can scryRenderedDOMComponentsWithClass with multiple classes
* should support injected wrapper components as DOM components
* should change the value of an input field
* should change the value of an input field in a component
* should throw when attempting to use ReactTestUtils.Simulate with shallow rendering
+26 -35
View File
@@ -13,7 +13,6 @@
'use strict';
import type { Fiber } from 'ReactFiber';
import type { HostChildren } from 'ReactFiberReconciler';
import type { ReactNodeList } from 'ReactTypes';
var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
@@ -39,6 +38,8 @@ var {
} = ReactDOMFiberComponent;
var { precacheFiberNode } = ReactDOMComponentTree;
const DOCUMENT_NODE = 9;
ReactDOMInjection.inject();
ReactControlledComponent.injection.injectFiberControlledHostComponent(
ReactDOMFiberComponent
@@ -54,23 +55,6 @@ type Props = { className ?: string };
type Instance = Element;
type TextInstance = Text;
function recursivelyAppendChildren(parent : Element, child : HostChildren<Instance | TextInstance>) {
if (!child) {
return;
}
/* $FlowFixMe: Element and Text should have this property. */
if (child.nodeType === 1 || child.nodeType === 3) {
/* $FlowFixMe: Refinement issue. I don't know how to express different. */
parent.appendChild(child);
} else {
/* As a result of the refinement issue this type isn't known. */
let node : any = child;
do {
recursivelyAppendChildren(parent, node.output);
} while (node = node.sibling);
}
}
let eventsEnabled : ?boolean = null;
let selectionInformation : ?mixed = null;
@@ -89,27 +73,28 @@ var DOMRenderer = ReactFiberReconciler({
eventsEnabled = null;
},
updateContainer(container : Container, children : HostChildren<Instance | TextInstance>) : void {
// TODO: Containers should update similarly to other parents.
container.innerHTML = '';
recursivelyAppendChildren(container, children);
},
createInstance(
type : string,
props : Props,
children : HostChildren<Instance | TextInstance>,
internalInstanceHandle : Object
) : Instance {
const root = document.body; // HACK
const root = document.documentElement; // HACK
const domElement : Instance = createElement(type, props, root);
precacheFiberNode(internalInstanceHandle, domElement);
recursivelyAppendChildren(domElement, children);
setInitialProperties(domElement, type, props, root);
return domElement;
},
appendInitialChild(parentInstance : Instance, child : Instance | TextInstance) : void {
parentInstance.appendChild(child);
},
finalizeInitialChildren(domElement : Instance, type : string, props : Props) : void {
const root = document.documentElement; // HACK
setInitialProperties(domElement, type, props, root);
},
prepareUpdate(
domElement : Instance,
oldProps : Props,
@@ -125,7 +110,7 @@ var DOMRenderer = ReactFiberReconciler({
internalInstanceHandle : Object
) : void {
var type = domElement.tagName.toLowerCase(); // HACK
var root = document.body; // HACK
var root = document.documentElement; // HACK
// Update the internal instance handle so that we know which props are
// the current ones.
precacheFiberNode(internalInstanceHandle, domElement);
@@ -142,19 +127,19 @@ var DOMRenderer = ReactFiberReconciler({
textInstance.nodeValue = newText;
},
appendChild(parentInstance : Instance, child : Instance | TextInstance) : void {
appendChild(parentInstance : Instance | Container, child : Instance | TextInstance) : void {
parentInstance.appendChild(child);
},
insertBefore(
parentInstance : Instance,
parentInstance : Instance | Container,
child : Instance | TextInstance,
beforeChild : Instance | TextInstance
) : void {
parentInstance.insertBefore(child, beforeChild);
},
removeChild(parentInstance : Instance, child : Instance | TextInstance) : void {
removeChild(parentInstance : Instance | Container, child : Instance | TextInstance) : void {
parentInstance.removeChild(child);
},
@@ -180,9 +165,15 @@ function warnAboutUnstableUse() {
warned = true;
}
function renderSubtreeIntoContainer(parentComponent : ?ReactComponent<any, any, any>, element : ReactElement<any>, container : DOMContainerElement, callback: ?Function) {
function renderSubtreeIntoContainer(parentComponent : ?ReactComponent<any, any, any>, element : ReactElement<any>, containerNode : DOMContainerElement | Document, callback: ?Function) {
let container : DOMContainerElement =
containerNode.nodeType === DOCUMENT_NODE ? (containerNode : any).documentElement : (containerNode : any);
let root;
if (!container._reactRootContainer) {
// First clear any existing content.
while (container.lastChild) {
container.removeChild(container.lastChild);
}
root = container._reactRootContainer = DOMRenderer.mountContainer(element, container, parentComponent, callback);
} else {
DOMRenderer.updateContainer(element, root = container._reactRootContainer, parentComponent, callback);
@@ -197,12 +188,12 @@ var ReactDOM = {
return renderSubtreeIntoContainer(null, element, container, callback);
},
unstable_renderSubtreeIntoContainer(parentComponent : ReactComponent<any, any, any>, element : ReactElement<any>, container : DOMContainerElement, callback: ?Function) {
unstable_renderSubtreeIntoContainer(parentComponent : ReactComponent<any, any, any>, element : ReactElement<any>, containerNode : DOMContainerElement | Document, callback: ?Function) {
invariant(
parentComponent != null && ReactInstanceMap.has(parentComponent),
'parentComponent must be a valid React Component'
);
return renderSubtreeIntoContainer(parentComponent, element, container, callback);
return renderSubtreeIntoContainer(parentComponent, element, containerNode, callback);
},
unmountComponentAtNode(container : DOMContainerElement) {
+24 -46
View File
@@ -21,7 +21,6 @@
import type { Fiber } from 'ReactFiber';
import type { UpdateQueue } from 'ReactFiberUpdateQueue';
import type { HostChildren } from 'ReactFiberReconciler';
var ReactFiberReconciler = require('ReactFiberReconciler');
var ReactInstanceMap = require('ReactInstanceMap');
@@ -32,59 +31,35 @@ var {
var scheduledAnimationCallback = null;
var scheduledDeferredCallback = null;
const TERMINAL_TAG = 99;
const TEXT_TAG = 98;
type Container = { rootID: string, children: Array<Instance | TextInstance> };
type Props = { prop: any };
type Instance = { tag: 99, type: string, id: number, children: Array<Instance | TextInstance>, prop: any };
type TextInstance = { tag: 98, text: string };
type Instance = {| type: string, id: number, children: Array<Instance | TextInstance>, prop: any |};
type TextInstance = {| text: string, id: number |};
var instanceCounter = 0;
function recursivelyAppendChildren(
flatArray : Array<Instance | TextInstance>,
child : HostChildren<Instance | TextInstance>
) {
if (!child) {
return;
}
if (child.tag === TERMINAL_TAG || child.tag === TEXT_TAG) {
flatArray.push(child);
} else {
let node = child;
do {
recursivelyAppendChildren(flatArray, node.output);
} while (node = node.sibling);
}
}
function flattenChildren(children : HostChildren<Instance | TextInstance>) {
const flatArray = [];
recursivelyAppendChildren(flatArray, children);
return flatArray;
}
var NoopRenderer = ReactFiberReconciler({
updateContainer(containerInfo : Container, children : HostChildren<Instance | TextInstance>) : void {
containerInfo.children = flattenChildren(children);
},
createInstance(type : string, props : Props, children : HostChildren<Instance | TextInstance>) : Instance {
createInstance(type : string, props : Props) : Instance {
const inst = {
tag: TERMINAL_TAG,
id: instanceCounter++,
type: type,
children: flattenChildren(children),
children: [],
prop: props.prop,
};
// Hide from unit tests
Object.defineProperty(inst, 'tag', { value: inst.tag, enumerable: false });
Object.defineProperty(inst, 'id', { value: inst.id, enumerable: false });
return inst;
},
appendInitialChild(parentInstance : Instance, child : Instance | TextInstance) : void {
parentInstance.children.push(child);
},
finalizeInitialChildren(domElement : Instance, type : string, props : Props) : void {
// Noop
},
prepareUpdate(instance : Instance, oldProps : Props, newProps : Props) : boolean {
return true;
},
@@ -94,9 +69,9 @@ var NoopRenderer = ReactFiberReconciler({
},
createTextInstance(text : string) : TextInstance {
var inst = { tag: TEXT_TAG, text : text };
var inst = { text : text, id: instanceCounter++ };
// Hide from unit tests
Object.defineProperty(inst, 'tag', { value: inst.tag, enumerable: false });
Object.defineProperty(inst, 'id', { value: inst.id, enumerable: false });
return inst;
},
@@ -104,7 +79,7 @@ var NoopRenderer = ReactFiberReconciler({
textInstance.text = newText;
},
appendChild(parentInstance : Instance, child : Instance | TextInstance) : void {
appendChild(parentInstance : Instance | Container, child : Instance | TextInstance) : void {
const index = parentInstance.children.indexOf(child);
if (index !== -1) {
parentInstance.children.splice(index, 1);
@@ -113,7 +88,7 @@ var NoopRenderer = ReactFiberReconciler({
},
insertBefore(
parentInstance : Instance,
parentInstance : Instance | Container,
child : Instance | TextInstance,
beforeChild : Instance | TextInstance
) : void {
@@ -128,7 +103,7 @@ var NoopRenderer = ReactFiberReconciler({
parentInstance.children.splice(beforeIndex, 0, child);
},
removeChild(parentInstance : Instance, child : Instance | TextInstance) : void {
removeChild(parentInstance : Instance | Container, child : Instance | TextInstance) : void {
const index = parentInstance.children.indexOf(child);
if (index === -1) {
throw new Error('This child does not exist.');
@@ -213,7 +188,7 @@ var ReactNoop = {
}
// Unsound duck typing.
const component = (componentOrElement : any);
if (component.tag === TERMINAL_TAG || component.tag === TEXT_TAG) {
if (typeof component.id === 'number') {
return component;
}
const inst = ReactInstanceMap.get(component);
@@ -278,10 +253,13 @@ var ReactNoop = {
function logHostInstances(children: Array<Instance | TextInstance>, depth) {
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (child.tag === TEXT_TAG) {
log(' '.repeat(depth) + '- ' + child.text);
var indent = ' '.repeat(depth);
if (typeof child.text === 'string') {
log(indent + '- ' + child.text);
} else {
log(' '.repeat(depth) + '- ' + child.type + '#' + child.id);
// $FlowFixMe - The child should've been refined now.
log(indent + '- ' + child.type + '#' + child.id);
// $FlowFixMe - The child should've been refined now.
logHostInstances(child.children, depth + 1);
}
}
@@ -300,7 +300,24 @@ module.exports = function<T, P, I, TI, C>(
}
function updatePortalComponent(current, workInProgress) {
reconcileChildren(current, workInProgress, workInProgress.pendingProps);
const priorityLevel = workInProgress.pendingWorkPriority;
const nextChildren = workInProgress.pendingProps;
if (!current) {
// Portals are special because we don't append the children during mount
// but at commit. Therefore we need to track insertions which the normal
// flow doesn't do during mount. This doesn't happen at the root because
// the root always starts with a "current" with a null child.
// TODO: Consider unifying this with how the root works.
workInProgress.child = reconcileChildFibersInPlace(
workInProgress,
workInProgress.child,
nextChildren,
priorityLevel
);
markChildAsProgressed(current, workInProgress, priorityLevel);
} else {
reconcileChildren(current, workInProgress, nextChildren);
}
}
/*
@@ -13,7 +13,6 @@
'use strict';
import type { Fiber } from 'ReactFiber';
import type { FiberRoot } from 'ReactFiberRoot';
import type { HostConfig } from 'ReactFiberReconciler';
var ReactTypeOfWork = require('ReactTypeOfWork');
@@ -37,7 +36,6 @@ module.exports = function<T, P, I, TI, C>(
trapError : (failedFiber : Fiber, error: Error, isUnmounting : boolean) => void
) {
const updateContainer = config.updateContainer;
const commitUpdate = config.commitUpdate;
const commitTextUpdate = config.commitTextUpdate;
@@ -68,20 +66,28 @@ module.exports = function<T, P, I, TI, C>(
}
}
function getHostParent(fiber : Fiber) : ?I {
function getHostParent(fiber : Fiber) : I | C {
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;
return parent.stateNode.containerInfo;
case Portal:
return parent.stateNode.containerInfo;
}
parent = parent.return;
}
return null;
throw new Error('Expected to find a host parent.');
}
function isHostParent(fiber : Fiber) : boolean {
return (
fiber.tag === HostComponent ||
fiber.tag === HostContainer ||
fiber.tag === Portal
);
}
function getHostSibling(fiber : Fiber) : ?I {
@@ -93,7 +99,7 @@ module.exports = function<T, P, I, TI, C>(
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 (!node.return || isHostParent(node.return)) {
// If we pop out of the root or hit the parent the fiber we are the
// last sibling.
return null;
@@ -128,9 +134,6 @@ module.exports = function<T, P, I, TI, C>(
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.
@@ -142,6 +145,10 @@ module.exports = function<T, P, I, TI, C>(
} else {
appendChild(parent, node.stateNode);
}
} else if (node.tag === Portal) {
// If the insertion itself is a portal, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
} else if (node.child) {
// TODO: Coroutines need to visit the stateNode.
node = node.child;
@@ -198,8 +205,14 @@ module.exports = function<T, P, I, TI, C>(
commitNestedUnmounts(node);
// After all the children have unmounted, it is now safe to remove the
// node from the tree.
if (parent) {
removeChild(parent, node.stateNode);
removeChild(parent, node.stateNode);
} else if (node.tag === Portal) {
// When we go into a portal, it becomes the parent to remove from.
// We will reassign it back when we pop the portal on the way up.
parent = node.stateNode.containerInfo;
if (node.child) {
node = node.child;
continue;
}
} else {
commitUnmount(node);
@@ -218,6 +231,11 @@ module.exports = function<T, P, I, TI, C>(
return;
}
node = node.return;
if (node.tag === Portal) {
// When we go out of the portal, we need to restore the parent.
// Since we don't keep a stack of them, we will search for it.
parent = getHostParent(node);
}
}
node.sibling.return = node.return;
node = node.sibling;
@@ -260,11 +278,6 @@ module.exports = function<T, P, I, TI, C>(
detachRef(current);
return;
}
case Portal: {
const containerInfo : C = current.stateNode.containerInfo;
updateContainer(containerInfo, null);
return;
}
}
}
@@ -274,14 +287,6 @@ module.exports = function<T, P, I, TI, C>(
detachRefIfNeeded(current, finishedWork);
return;
}
case HostContainer: {
// TODO: Attach children to root container.
const children = finishedWork.output;
const root : FiberRoot = finishedWork.stateNode;
const containerInfo : C = root.containerInfo;
updateContainer(containerInfo, children);
return;
}
case HostComponent: {
const instance : I = finishedWork.stateNode;
if (instance != null && current) {
@@ -303,10 +308,10 @@ module.exports = function<T, P, I, TI, C>(
commitTextUpdate(textInstance, oldText, newText);
return;
}
case HostContainer: {
return;
}
case Portal: {
const children = finishedWork.child;
const containerInfo : C = finishedWork.stateNode.containerInfo;
updateContainer(containerInfo, children);
return;
}
default:
@@ -46,6 +46,8 @@ var {
module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
const createInstance = config.createInstance;
const appendInitialChild = config.appendInitialChild;
const finalizeInitialChildren = config.finalizeInitialChildren;
const createTextInstance = config.createTextInstance;
const prepareUpdate = config.prepareUpdate;
@@ -124,6 +126,35 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
return workInProgress.stateNode;
}
function appendAllChildren(parent : I, workInProgress : Fiber) {
// We only have the top Fiber that was created but we need recurse down its
// children to find all the terminal nodes.
let node = workInProgress.child;
while (node) {
if (node.tag === HostComponent || node.tag === HostText) {
appendInitialChild(parent, node.stateNode);
} else if (node.tag === Portal) {
// If we have a portal child, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
} else if (node.child) {
// TODO: Coroutines need to visit the stateNode.
node = node.child;
continue;
}
if (node === workInProgress) {
return;
}
while (!node.sibling) {
if (!node.return || node.return === workInProgress) {
return;
}
node = node.return;
}
node = node.sibling;
}
}
function completeWork(current : ?Fiber, workInProgress : Fiber) : ?Fiber {
switch (workInProgress.tag) {
case FunctionalComponent:
@@ -167,11 +198,8 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
fiberRoot.context = fiberRoot.pendingContext;
fiberRoot.pendingContext = null;
}
// We don't know if a container has updated any children so we always
// need to update it right now. We schedule this side-effect before
// all the other side-effects in the subtree. We need to schedule it
// before so that the entire tree is up-to-date before the life-cycles
// are invoked.
// TODO: Only mark this as an update if we have any pending callbacks
// on it.
markUpdate(workInProgress);
return null;
}
@@ -204,9 +232,16 @@ 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, workInProgress);
// TODO: Move createInstance to beginWork and keep it on a context
// "stack" as the parent. Then append children as we go in beginWork
// or completeWork depending on we want to add then top->down or
// bottom->up. Top->down is faster in IE11.
// Finally, finalizeInitialChildren here in completeWork.
const instance = createInstance(workInProgress.type, newProps, workInProgress);
appendAllChildren(instance, workInProgress);
finalizeInitialChildren(instance, workInProgress.type, newProps);
// TODO: This seems like unnecessary duplication.
workInProgress.stateNode = instance;
workInProgress.output = instance;
@@ -253,6 +288,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
transferOutput(workInProgress.child, workInProgress);
return null;
case Portal:
// TODO: Only mark this as an update if we have any pending callbacks.
markUpdate(workInProgress);
workInProgress.output = null;
workInProgress.memoizedProps = workInProgress.pendingProps;
@@ -47,22 +47,19 @@ type OpaqueNode = Fiber;
export type HostConfig<T, P, I, TI, C> = {
// TODO: We don't currently have a quick way to detect that children didn't
// reorder so we host will always need to check the set. We should make a flag
// or something so that it can bailout easily.
createInstance(type : T, props : P, internalInstanceHandle : OpaqueNode) : I,
appendInitialChild(parentInstance : I, child : I) : void,
finalizeInitialChildren(parentInstance : I, type : T, props : P) : void,
updateContainer(containerInfo : C, children : HostChildren<I | TI>) : void,
createInstance(type : T, props : P, children : HostChildren<I | TI>, internalInstanceHandle : OpaqueNode) : I,
prepareUpdate(instance : I, oldProps : P, newProps : P) : boolean,
commitUpdate(instance : I, oldProps : P, newProps : P, internalInstanceHandle : OpaqueNode) : void,
createTextInstance(text : string, internalInstanceHandle : OpaqueNode) : 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,
appendChild(parentInstance : I | C, child : I | TI) : void,
insertBefore(parentInstance : I | C, child : I | TI, beforeChild : I | TI) : void,
removeChild(parentInstance : I | C, child : I | TI) : void,
scheduleAnimationCallback(callback : () => void) : void,
scheduleDeferredCallback(callback : (deadline : Deadline) => void) : void,