Apply side-effects to host containers

This updates the host container root with new children.
Currently, this is always called for updates because we don't
track if any children reordered.
This commit is contained in:
Sebastian Markbage
2016-06-30 00:58:43 -07:00
parent 62d4561910
commit 2f0ff6e974
10 changed files with 213 additions and 27 deletions

View File

@@ -34,4 +34,4 @@ suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(2[0-4]\\|1[0-9]\\|[0-9
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
[version]
^0.26.0
^0.27.0

View File

@@ -43,7 +43,7 @@
"eslint-plugin-react-internal": "file:eslint-rules",
"fbjs": "^0.8.1",
"fbjs-scripts": "^0.6.0",
"flow-bin": "^0.26.0",
"flow-bin": "^0.27.0",
"glob": "^6.0.1",
"grunt": "^0.4.5",
"grunt-cli": "^0.1.13",

View File

@@ -27,18 +27,53 @@ var ReactFiberReconciler = require('ReactFiberReconciler');
var scheduledHighPriCallback = null;
var scheduledLowPriCallback = null;
const TERMINAL_TAG = 99;
type Container = { rootID: number, children: Array<Instance> };
type Props = { };
type Instance = { id: number };
type Instance = { tag: 99, type: string, id: number, children: Array<Instance> };
var instanceCounter = 0;
function recursivelyAppendChildren(flatArray : Array<Instance>, child : HostChildren<Instance>) {
if (!child) {
return;
}
if (child.tag === TERMINAL_TAG) {
flatArray.push(child);
} else {
let node = child;
do {
recursivelyAppendChildren(flatArray, node.output);
} while (node = node.sibling);
}
}
function flattenChildren(children : HostChildren<Instance>) {
const flatArray = [];
recursivelyAppendChildren(flatArray, children);
return flatArray;
}
var NoopRenderer = ReactFiberReconciler({
updateContainer(containerInfo : Container, children : HostChildren<Instance>) : void {
console.log('Update container #' + containerInfo.rootID);
containerInfo.children = flattenChildren(children);
},
createInstance(type : string, props : Props, children : HostChildren<Instance>) : Instance {
console.log('Create instance #' + instanceCounter);
return {
id: instanceCounter++
const inst = {
tag: TERMINAL_TAG,
id: instanceCounter++,
type: type,
children: flattenChildren(children),
};
// Hide from unit tests
Object.defineProperty(inst, 'tag', { value: inst.tag, enumerable: false });
Object.defineProperty(inst, 'id', { value: inst.id, enumerable: false });
return inst;
},
prepareUpdate(instance : Instance, oldProps : Props, newProps : Props, children : HostChildren<Instance>) : boolean {
@@ -48,6 +83,7 @@ var NoopRenderer = ReactFiberReconciler({
commitUpdate(instance : Instance, oldProps : Props, newProps : Props, children : HostChildren<Instance>) : void {
console.log('Commit update on #' + instance.id);
instance.children = flattenChildren(children);
},
deleteInstance(instance : Instance) : void {
@@ -64,13 +100,17 @@ var NoopRenderer = ReactFiberReconciler({
});
var rootContainer = { rootID: 0, children: [] };
var root = null;
var ReactNoop = {
root: rootContainer,
render(element : ReactElement<any>) {
if (!root) {
root = NoopRenderer.mountContainer(element, null);
root = NoopRenderer.mountContainer(element, rootContainer);
} else {
NoopRenderer.updateContainer(element, root);
}
@@ -115,6 +155,19 @@ var ReactNoop = {
console.log('Nothing rendered yet.');
return;
}
function logHostInstances(children: Array<Instance>, depth) {
for (var i = 0; i < children.length; i++) {
var child = children[i];
console.log(' '.repeat(depth) + '- ' + child.type + '#' + child.id);
logHostInstances(child.children, depth + 1);
}
}
function logContainer(container : Container, depth) {
console.log(' '.repeat(depth) + '- [root#' + container.rootID + ']');
logHostInstances(container.children, depth + 1);
}
function logFiber(fiber : Fiber, depth) {
console.log(' '.repeat(depth) + '- ' + (fiber.type ? fiber.type.name || fiber.type : '[root]'), '[' + fiber.pendingWorkPriority + (fiber.pendingProps ? '*' : '') + ']');
if (fiber.child) {
@@ -124,6 +177,10 @@ var ReactNoop = {
logFiber(fiber.sibling, depth);
}
}
console.log('HOST INSTANCES:');
logContainer(rootContainer, 0);
console.log('FIBERS:');
logFiber((root.stateNode : any).current, 0);
},

View File

@@ -34,7 +34,7 @@ var {
} = require('ReactPriorityLevel');
var { findNextUnitOfWorkAtPriority } = require('ReactFiberPendingWork');
module.exports = function<T, P, I>(config : HostConfig<T, P, I>) {
module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
function reconcileChildren(current, workInProgress, nextChildren) {
const priority = workInProgress.pendingWorkPriority;

View File

@@ -13,6 +13,7 @@
'use strict';
import type { Fiber } from 'ReactFiber';
import type { FiberRoot } from 'ReactFiberRoot';
import type { HostConfig } from 'ReactFiberReconciler';
var ReactTypeOfWork = require('ReactTypeOfWork');
@@ -22,23 +23,31 @@ var {
HostComponent,
} = ReactTypeOfWork;
module.exports = function<T, P, I>(config : HostConfig<T, P, I>) {
module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
const updateContainer = config.updateContainer;
const commitUpdate = config.commitUpdate;
function commitWork(finishedWork : Fiber) : void {
switch (finishedWork.tag) {
case ClassComponent:
case ClassComponent: {
// TODO: Fire componentDidMount/componentDidUpdate, update refs
return;
case HostContainer:
}
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:
}
case HostComponent: {
if (finishedWork.stateNode == null || !finishedWork.alternate) {
throw new Error('This should only be done during updates.');
}
const children = finishedWork.output;
const child = (finishedWork.child : ?Fiber);
const children = (child && !child.sibling) ? (child.output : ?Fiber | I) : child;
const newProps = finishedWork.memoizedProps;
// If we have an alternate, that means this is an update and we need to
// schedule a side-effect to do the updates.
@@ -47,6 +56,7 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) {
const instance : I = finishedWork.stateNode;
commitUpdate(instance, oldProps, newProps, children);
return;
}
default:
throw new Error('This unit of work tag should not have side-effects.');
}

View File

@@ -17,6 +17,8 @@ import type { Fiber } from 'ReactFiber';
import type { HostConfig } from 'ReactFiberReconciler';
import type { ReifiedYield } from 'ReactReifiedYield';
import type { TypeOfWork } from 'ReactTypeOfWork';
var ReactChildFiber = require('ReactChildFiber');
var ReactTypeOfWork = require('ReactTypeOfWork');
var {
@@ -30,7 +32,7 @@ var {
YieldComponent,
} = ReactTypeOfWork;
module.exports = function<T, P, I>(config : HostConfig<T, P, I>) {
module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
const createInstance = config.createInstance;
const prepareUpdate = config.prepareUpdate;
@@ -120,12 +122,22 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) {
transferOutput(workInProgress.child, workInProgress);
return null;
case HostContainer:
transferOutput(workInProgress.child, workInProgress);
// 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 after
// all the other side-effects in the subtree.
// TODO: I just realized that this means that nodes are not in the DOM
// document when componentDidMount/Update fires. I'll need to change
// the order for host component effects to happen *before* their
// children I think.
markForPostEffect(workInProgress);
return null;
case HostComponent:
console.log('/host component', workInProgress.type);
transferOutput(workInProgress.child, workInProgress);
const children = workInProgress.output;
const newProps = workInProgress.memoizedProps;
const child = (workInProgress.child : ?Fiber);
const children = (child && !child.sibling) ? (child.output : ?Fiber | I) : child;
const newProps = workInProgress.pendingProps;
workInProgress.memoizedProps = newProps;
if (workInProgress.alternate && 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.
@@ -136,9 +148,12 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) {
// This returns true if there was something to update.
markForPostEffect(workInProgress);
}
workInProgress.output = instance;
} else {
const instance = createInstance(workInProgress.type, newProps, children);
// TODO: This seems like unnecessary duplication.
workInProgress.stateNode = instance;
workInProgress.output = instance;
}
return null;
case CoroutineComponent:

View File

@@ -14,6 +14,7 @@
import type { Fiber } from 'ReactFiber';
import type { FiberRoot } from 'ReactFiberRoot';
import type { TypeOfWork } from 'ReactTypeOfWork';
var { createFiberRoot } = require('ReactFiberRoot');
var ReactFiberScheduler = require('ReactFiberScheduler');
@@ -26,11 +27,17 @@ type Deadline = {
timeRemaining : () => number
};
type HostChildNode<I> = { output: HostChildren<I>, sibling: ?HostChildNode<I> };
type HostChildNode<I> = { tag: TypeOfWork, output: HostChildren<I>, sibling: any };
export type HostChildren<I> = null | void | I | HostChildNode<I>;
export type HostConfig<T, P, I> = {
export type HostConfig<T, P, I, 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.
updateContainer(containerInfo : C, children : HostChildren<I>) : void;
createInstance(type : T, props : P, children : HostChildren<I>) : I,
prepareUpdate(instance : I, oldProps : P, newProps : P, children : HostChildren<I>) : bool,
@@ -44,22 +51,22 @@ export type HostConfig<T, P, I> = {
type OpaqueNode = Fiber;
export type Reconciler = {
mountContainer(element : ReactElement<any>, containerInfo : ?Object) : OpaqueNode,
export type Reconciler<C> = {
mountContainer(element : ReactElement<any>, containerInfo : C) : OpaqueNode,
updateContainer(element : ReactElement<any>, container : OpaqueNode) : void,
unmountContainer(container : OpaqueNode) : void,
// Used to extract the return value from the initial render. Legacy API.
getPublicRootInstance(container : OpaqueNode) : ?Object,
getPublicRootInstance(container : OpaqueNode) : (C | null),
};
module.exports = function<T, P, I>(config : HostConfig<T, P, I>) : Reconciler {
module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) : Reconciler<C> {
var { scheduleLowPriWork } = ReactFiberScheduler(config);
return {
mountContainer(element : ReactElement<any>, containerInfo : ?Object) : OpaqueNode {
mountContainer(element : ReactElement<any>, containerInfo : C) : OpaqueNode {
const root = createFiberRoot(containerInfo);
const container = root.current;
// TODO: Use pending work/state instead of props.
@@ -94,7 +101,7 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) : Reconciler {
scheduleLowPriWork(root);
},
getPublicRootInstance(container : OpaqueNode) : ?Object {
getPublicRootInstance(container : OpaqueNode) : (C | null) {
return null;
},

View File

@@ -18,7 +18,7 @@ const { createHostContainerFiber } = require('ReactFiber');
export type FiberRoot = {
// Any additional information from the host associated with this root.
containerInfo: ?Object,
containerInfo: any,
// The currently active root fiber. This is the mutable root of the tree.
current: Fiber,
// Determines if this root has already been added to the schedule for work.
@@ -27,7 +27,7 @@ export type FiberRoot = {
nextScheduledRoot: ?FiberRoot,
};
exports.createFiberRoot = function(containerInfo : ?Object) : FiberRoot {
exports.createFiberRoot = function(containerInfo : any) : FiberRoot {
// Cyclic construction. This cheats the type system right now because
// stateNode is any.
const uninitializedFiber = createHostContainerFiber();

View File

@@ -32,7 +32,7 @@ var {
var timeHeuristicForUnitOfWork = 1;
module.exports = function<T, P, I>(config : HostConfig<T, P, I>) {
module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
const { beginWork } = ReactFiberBeginWork(config);
const { completeWork } = ReactFiberCompleteWork(config);

View File

@@ -0,0 +1,97 @@
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @emails react-core
*/
'use strict';
var React;
var ReactNoop;
describe('ReactIncremental', function() {
beforeEach(function() {
React = require('React');
ReactNoop = require('ReactNoop');
spyOn(console, 'log');
});
function div(...children) {
return { type: 'div', children };
}
function span(...children) {
return { type: 'span', children };
}
it('can update child nodes of a host instance', function() {
function Bar(props) {
return <span>{props.text}</span>;
}
function Foo(props) {
return (
<div>
<Bar text={props.text} />
{props.text === 'World' ? <Bar text={props.text} /> : null}
</div>
);
}
ReactNoop.render(<Foo text="Hello" />);
ReactNoop.flush();
expect(ReactNoop.root.children).toEqual([
div(span()),
]);
ReactNoop.render(<Foo text="World" />);
ReactNoop.flush();
expect(ReactNoop.root.children).toEqual([
div(span(), span()),
]);
});
it('does not update child nodes if a flush is aborted', function() {
function Bar(props) {
return <span>{props.text}</span>;
}
function Foo(props) {
return (
<div>
<div>
<Bar text={props.text} />
{props.text === 'Hello' ? <Bar text={props.text} /> : null}
</div>
<Bar text="Yo" />
</div>
);
}
ReactNoop.render(<Foo text="Hello" />);
ReactNoop.flush();
expect(ReactNoop.root.children).toEqual([
div(div(span(), span()), span()),
]);
ReactNoop.render(<Foo text="World" />);
ReactNoop.flushLowPri(35);
expect(ReactNoop.root.children).toEqual([
div(div(span(), span()), span()),
]);
});
// TODO: Test that side-effects are not cut off when a work in progress node
// moves to "current" without flushing due to having lower priority. Does this
// even happen? Maybe a child doesn't get processed because it is lower prio?
});