Coroutines

This commit is contained in:
Sebastian Markbage
2016-05-26 18:13:21 -07:00
parent 7c8a090994
commit fd4f74ef95
12 changed files with 473 additions and 92 deletions
@@ -398,4 +398,6 @@ ReactElement.isValidElement = function(object) {
);
};
ReactElement.REACT_ELEMENT_TYPE = REACT_ELEMENT_TYPE;
module.exports = ReactElement;
@@ -184,7 +184,8 @@ function validatePropTypes(element) {
var ReactElementValidator = {
createElement: function(type, props, children) {
var validType = typeof type === 'string' || typeof type === 'function';
var validType = typeof type === 'string' || typeof type === 'function' ||
(type !== null && typeof type === 'object');
// We warn in this case but don't throw. We expect the element creation to
// succeed and there will likely be errors in render.
warning(
+50 -5
View File
@@ -13,14 +13,17 @@
var React;
var ReactNoop;
var ReactCoroutine;
describe('ReactComponent', function() {
beforeEach(function() {
React = require('React');
ReactNoop = require('ReactNoop');
ReactCoroutine = require('ReactCoroutine');
spyOn(console, 'log');
});
/*it('should render a simple component', function() {
it('should render a simple component', function() {
function Bar() {
return <div>Hello World</div>;
@@ -33,7 +36,7 @@ describe('ReactComponent', function() {
ReactNoop.render(<Foo />);
ReactNoop.flush();
});*/
});
it('should render a simple component, in steps if needed', function() {
@@ -49,12 +52,54 @@ describe('ReactComponent', function() {
}
ReactNoop.render(<Foo />);
console.log('Nothing done');
// console.log('Nothing done');
ReactNoop.flushLowPri(7);
console.log('Yield');
// console.log('Yield');
ReactNoop.flushLowPri(50);
console.log('Done');
// console.log('Done');
});
it('should render a coroutine', function() {
function Continuation({ isSame }) {
return <span>{isSame ? 'foo==bar' : 'foo!=bar'}</span>;
}
// An alternative API could mark Continuation as something that needs
// yielding. E.g. Continuation.yieldType = 123;
function Child({ bar }) {
return ReactCoroutine.createYield({
bar: bar,
}, Continuation, null);
}
function Indirection() {
return [<Child bar={true} />, <Child bar={false} />];
}
function HandleYields(props, yields) {
return yields.map(y =>
<y.continuation isSame={props.foo === y.props.bar} />
);
}
// An alternative API could mark Parent as something that needs
// yielding. E.g. Parent.handler = HandleYields;
function Parent(props) {
return ReactCoroutine.createCoroutine(
props.children,
HandleYields,
props
);
}
function App() {
return <div><Parent foo={true}><Indirection /></Parent></div>;
}
ReactNoop.render(<App />);
ReactNoop.flush();
});
});
+70 -27
View File
@@ -12,33 +12,53 @@
'use strict';
import type { ReactCoroutine, ReactYield } from 'ReactCoroutine';
import type { Fiber } from 'ReactFiber';
var ReactElement = require('ReactElement');
import type { ReactNodeList } from 'ReactTypes';
var {
REACT_ELEMENT_TYPE,
} = require('ReactElement');
var {
REACT_COROUTINE_TYPE,
REACT_YIELD_TYPE,
} = require('ReactCoroutine');
var ReactFiber = require('ReactFiber');
type ReactNode = ReactElement | ReactFragment | ReactText;
type ReactFragment = Iterable<ReactNode | ReactEmpty>;
type ReactNodeList = ReactNode | ReactEmpty;
type ReactText = string | number;
type ReactEmpty = null | void | boolean;
var ReactReifiedYield = require('ReactReifiedYield');
function createSubsequentChild(parent : Fiber, previousSibling : Fiber, newChildren) : Fiber {
if (typeof newChildren !== 'object' || newChildren === null) {
return previousSibling;
}
if (ReactElement.isValidElement(newChildren)) {
var element = (newChildren : ReactElement);
var child = ReactFiber.createFiberFromElement(element);
previousSibling.sibling = child;
child.parent = parent;
return child;
switch (newChildren.$$typeof) {
case REACT_ELEMENT_TYPE: {
const element = (newChildren : ReactElement);
const child = ReactFiber.createFiberFromElement(element);
previousSibling.sibling = child;
child.parent = parent;
return child;
}
case REACT_COROUTINE_TYPE: {
const coroutine = (newChildren : ReactCoroutine);
const child = ReactFiber.createFiberFromCoroutine(coroutine);
previousSibling.sibling = child;
child.parent = parent;
return child;
}
case REACT_YIELD_TYPE: {
const yieldNode = (newChildren : ReactYield);
const reifiedYield = ReactReifiedYield.createReifiedYield(yieldNode);
const child = ReactFiber.createFiberFromYield(yieldNode);
child.output = reifiedYield;
previousSibling.sibling = child;
child.parent = parent;
return child;
}
}
if (Array.isArray(newChildren)) {
@@ -48,39 +68,62 @@ function createSubsequentChild(parent : Fiber, previousSibling : Fiber, newChild
}
return prev;
} else {
console.log('Unknown child', newChildren);
return previousSibling;
}
}
function createFirstChild(parent, newChildren) {
if (typeof newChildren !== 'object' || newChildren === null) {
parent.child = null;
return null;
}
if (ReactElement.isValidElement(newChildren)) {
var element = (newChildren : ReactElement);
var child = ReactFiber.createFiberFromElement(element);
parent.child = child;
child.parent = parent;
return child;
switch (newChildren.$$typeof) {
case REACT_ELEMENT_TYPE: {
const element = (newChildren : ReactElement);
const child = ReactFiber.createFiberFromElement(element);
child.parent = parent;
return child;
}
case REACT_COROUTINE_TYPE: {
const coroutine = (newChildren : ReactCoroutine);
const child = ReactFiber.createFiberFromCoroutine(coroutine);
child.parent = parent;
return child;
}
case REACT_YIELD_TYPE: {
// A yield results in a fragment fiber whose output is the continuation.
// TODO: When there is only a single child, we can optimize this to avoid
// the fragment.
const yieldNode = (newChildren : ReactYield);
const reifiedYield = ReactReifiedYield.createReifiedYield(yieldNode);
const child = ReactFiber.createFiberFromYield(yieldNode);
child.output = reifiedYield;
child.parent = parent;
return child;
}
}
if (Array.isArray(newChildren)) {
var first : ?Fiber = null;
var prev : ?Fiber = null;
for (var i = 0; i < newChildren.length; i++) {
if (prev == null) {
prev = createFirstChild(parent, newChildren[i]);
first = prev;
} else {
prev = createSubsequentChild(parent, prev, newChildren[i]);
}
}
return first;
} else {
parent.child = null;
console.log('Unknown child', newChildren);
return null;
}
}
exports.reconcileChildFibers = function(parent : Fiber, newChildren : ReactNodeList) : void {
createFirstChild(parent, newChildren);
exports.reconcileChildFibers = function(parent : Fiber, firstChild : ?Fiber, newChildren : ReactNodeList) : ?Fiber {
return createFirstChild(parent, newChildren);
};
+55 -19
View File
@@ -17,30 +17,42 @@ var {
IndeterminateComponent,
ClassComponent,
HostComponent,
CoroutineComponent,
YieldComponent,
} = ReactTypesOfWork;
type StateNode = {};
type EffectHandler = () => void;
type EffectTag = number;
var ReactElement = require('ReactElement');
import type { ReactCoroutine, ReactYield } from 'ReactCoroutine';
export type Fiber = {
// Tag identifying the type of fiber.
tag: number,
parent: ?Fiber,
// Singly Linked List Tree Structure.
parent: ?Fiber, // Consider a regenerated temporary parent stack instead.
child: ?Fiber,
sibling: ?Fiber,
input: ?Object,
output: ?Object,
// Input is the data coming into process this fiber. Arguments.
input: any, // This type will be more specific once we overload the tag.
// 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.
// Used by multi-stage coroutines.
stage: number, // Consider reusing the tag field instead.
// This will be used to quickly determine if a subtree has no pending changes.
hasPendingChanges: bool,
stateNode: StateNode,
// The local state associated with this fiber.
stateNode: ?Object,
};
var createFiber = function(tag : number, handlerTag : number) : Fiber {
var createFiber = function(tag : number) : Fiber {
return {
tag: tag,
@@ -52,9 +64,11 @@ var createFiber = function(tag : number, handlerTag : number) : Fiber {
input: null,
output: null,
stage: 0,
hasPendingChanges: true,
stateNode: {},
stateNode: null,
};
};
@@ -64,17 +78,39 @@ function shouldConstruct(Component) {
}
exports.createFiberFromElement = function(element : ReactElement) {
let fiber;
if (typeof element.type === 'function') {
fiber = shouldConstruct(element.type) ?
createFiber(ClassComponent, 0) :
createFiber(IndeterminateComponent, 0);
} else if (typeof element.type === 'string') {
fiber = createFiber(HostComponent, 1);
} else {
throw new Error('Unknown component type: ' + typeof element.type);
const fiber = exports.createFiberFromElementType(element.type);
if (typeof element.type === 'object') {
// Hacky McHack
element = ReactElement(fiber.input, null, element.ref, null, null, null, element.props);
}
fiber.input = element;
return fiber;
};
exports.createFiberFromElementType = function(type : mixed) {
let fiber;
if (typeof type === 'function') {
fiber = shouldConstruct(type) ?
createFiber(ClassComponent) :
createFiber(IndeterminateComponent);
} else if (typeof type === 'string') {
fiber = createFiber(HostComponent);
} else if (typeof type === 'object' && type !== null) {
// Currently assumed to be a continuation and therefore is a fiber already.
fiber = type;
} else {
throw new Error('Unknown component type: ' + typeof type);
}
return fiber;
};
exports.createFiberFromCoroutine = function(coroutine : ReactCoroutine) {
const fiber = createFiber(CoroutineComponent);
fiber.input = coroutine;
return fiber;
};
exports.createFiberFromYield = function(yieldNode : ReactYield) {
const fiber = createFiber(YieldComponent);
return fiber;
};
@@ -12,6 +12,7 @@
'use strict';
import type { ReactCoroutine } from 'ReactCoroutine';
import type { Fiber } from 'ReactFiber';
var ReactChildFiber = require('ReactChildFiber');
@@ -21,6 +22,8 @@ var {
FunctionalComponent,
ClassComponent,
HostComponent,
CoroutineComponent,
YieldComponent,
} = ReactTypesOfWork;
function getElement(unitOfWork) : ReactElement {
@@ -38,19 +41,21 @@ function updateFunctionalComponent(unitOfWork) {
console.log('perform work on:', fn.name);
var nextChildren = fn(props);
ReactChildFiber.reconcileChildFibers(
unitOfWork.child = ReactChildFiber.reconcileChildFibers(
unitOfWork,
unitOfWork.child,
nextChildren
);
}
function updateHostComponent(unitOfWork) {
var element = getElement(unitOfWork);
console.log('host component', element.type);
console.log('host component', element.type, typeof element.props.children === 'string' ? element.props.children : '');
var nextChildren = element.props.children;
ReactChildFiber.reconcileChildFibers(
unitOfWork.child = ReactChildFiber.reconcileChildFibers(
unitOfWork,
unitOfWork.child,
nextChildren
);
}
@@ -69,13 +74,27 @@ function mountIndeterminateComponent(unitOfWork) {
// Proceed under the assumption that this is a functional component
unitOfWork.tag = FunctionalComponent;
}
ReactChildFiber.reconcileChildFibers(
unitOfWork.child = ReactChildFiber.reconcileChildFibers(
unitOfWork,
unitOfWork.child,
value
);
}
exports.beginWork = function(unitOfWork : Fiber) : ?Fiber {
function updateCoroutineComponent(unitOfWork) {
var coroutine = (unitOfWork.input : ?ReactCoroutine);
if (!coroutine) {
throw new Error('Should be resolved by now');
}
console.log('begin coroutine', coroutine.handler.name);
unitOfWork.child = ReactChildFiber.reconcileChildFibers(
unitOfWork,
unitOfWork.child,
coroutine.children
);
}
function beginWork(unitOfWork : Fiber) : ?Fiber {
switch (unitOfWork.tag) {
case IndeterminateComponent:
mountIndeterminateComponent(unitOfWork);
@@ -84,14 +103,32 @@ exports.beginWork = function(unitOfWork : Fiber) : ?Fiber {
updateFunctionalComponent(unitOfWork);
break;
case ClassComponent:
// $FlowFixMe
console.log('class component', unitOfWork.input.type.name);
break;
case HostComponent:
updateHostComponent(unitOfWork);
break;
case CoroutineComponent:
// Reset the stage to zero.
unitOfWork.stage = 0;
updateCoroutineComponent(unitOfWork);
// 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);
}
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);
}
return null;
default:
throw new Error('Unknown unit of work tag');
}
return unitOfWork.child;
};
}
exports.beginWork = beginWork;
@@ -12,8 +12,11 @@
'use strict';
import type { ReactCoroutine } from 'ReactCoroutine';
import type { Fiber } from 'ReactFiber';
import type { ReifiedYield } from 'ReactReifiedYield';
var ReactChildFiber = require('ReactChildFiber');
var ReactTypesOfWork = require('ReactTypesOfWork');
var {
@@ -21,22 +24,94 @@ var {
FunctionalComponent,
ClassComponent,
HostComponent,
CoroutineComponent,
YieldComponent,
} = ReactTypesOfWork;
function transferOutput(child : ?Fiber, parent : Fiber) {
// If we have a single result, we just pass that through as the output to
// 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;
}
function recursivelyFillYields(yields, output : ?Fiber | ?ReifiedYield) {
if (!output) {
// Ignore nulls etc.
} else if (output.tag !== undefined) { // TODO: Fix this fragile duck test.
// Detect if this is a fiber, if so it is a fragment result.
// $FlowFixMe: Refinement issue.
var item = (output : Fiber);
do {
recursivelyFillYields(yields, item.output);
item = item.sibling;
} while (item);
} else {
// $FlowFixMe: Refinement issue. If it is not a Fiber or null, it is a yield
yields.push(output);
}
}
function handleCoroutine(unitOfWork : Fiber) {
var coroutine = (unitOfWork.input : ?ReactCoroutine);
if (!coroutine) {
throw new Error('Should be resolved by now');
}
if (unitOfWork.stage === 0) {
// First step of the coroutine has completed. Now we need to do the second.
// TODO: It would be nice to have a multi stage coroutine represented by a
// single component, or at least tail call optimize nested ones.
// TODO: If we end up not using multi stage coroutines, we could also reuse
// the tag field to switch between the two stages.
unitOfWork.stage = 1;
// Build up the yields.
// TODO: Compare this to a generator or opaque helpers like Children.
var yields : Array<ReifiedYield> = [];
var child = unitOfWork.child;
while (child) {
recursivelyFillYields(yields, child.output);
child = child.sibling;
}
var fn = coroutine.handler;
var props = coroutine.props;
var nextChildren = fn(props, yields);
unitOfWork.stateNode = ReactChildFiber.reconcileChildFibers(
unitOfWork,
unitOfWork.stateNode,
nextChildren
);
return unitOfWork.stateNode;
} else {
// The coroutine is now complete.
transferOutput(unitOfWork.stateNode, unitOfWork);
return null;
}
}
exports.completeWork = function(unitOfWork : Fiber) : ?Fiber {
switch (unitOfWork.tag) {
case FunctionalComponent:
// $FlowFixMe
console.log('/functional component', unitOfWork.input.type.name);
transferOutput(unitOfWork.child, unitOfWork);
break;
case ClassComponent:
// $FlowFixMe
console.log('/class component', unitOfWork.input.type.name);
transferOutput(unitOfWork.child, unitOfWork);
break;
case HostComponent:
// $FlowFixMe
console.log('/host component', unitOfWork.input.type);
break;
case CoroutineComponent:
console.log('/coroutine component', unitOfWork.input.handler.name);
return handleCoroutine(unitOfWork);
case YieldComponent:
// Does nothing.
break;
// Error cases
case IndeterminateComponent:
throw new Error('An indeterminate component should have become determinate before completing.');
default:
@@ -0,0 +1,37 @@
/**
* 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.
*
* @providesModule ReactReifiedYield
* @flow
*/
'use strict';
import type { ReactYield } from 'ReactCoroutine';
import type { Fiber } from 'ReactFiber';
var ReactFiber = require('ReactFiber');
export type ReifiedYield = { continuation: Fiber, props: Object };
exports.createReifiedYield = function(yieldNode : ReactYield) : ReifiedYield {
var fiber = ReactFiber.createFiberFromElementType(yieldNode.continuation);
// Hacky way to store the continuation
fiber.input = yieldNode.continuation;
return {
continuation: fiber,
props: yieldNode.props,
};
};
exports.createUpdatedReifiedYield = function(previousYield : ReifiedYield, yieldNode : ReactYield) : ReifiedYield {
return {
continuation: previousYield.continuation,
props: yieldNode.props,
};
};
@@ -1,23 +0,0 @@
/**
* 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.
*
* @providesModule ReactStateNode
* @flow
*/
'use strict';
type StateNode = {
next: ?{ [key: string]: StateNode },
};
module.exports = function() : StateNode {
return {
next: null,
};
};
@@ -0,0 +1,109 @@
/**
* Copyright 2014-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.
*
* @providesModule ReactCoroutine
* @flow
*/
'use strict';
import type { ReactNodeList } from 'ReactTypes';
// The Symbol used to tag the special React types. If there is no native Symbol
// nor polyfill, then a plain number is used for performance.
var REACT_COROUTINE_TYPE =
(typeof Symbol === 'function' && Symbol.for && Symbol.for('react.coroutine')) ||
0xeac8;
var REACT_YIELD_TYPE =
(typeof Symbol === 'function' && Symbol.for && Symbol.for('react.yield')) ||
0xeac9;
type ReifiedYield = { continuation: Object, props: Object };
type CoroutineHandler<T> = (props: T, yields: Array<ReifiedYield>) => ReactNodeList;
export type ReactCoroutine = {
$$typeof: Symbol | number,
key: ?string,
children: any,
// This should be a more specific CoroutineHandler
handler: (props: any, yields: Array<ReifiedYield>) => ReactNodeList,
props: mixed,
};
export type ReactYield = {
$$typeof: Symbol | number,
key: ?string,
props: Object,
continuation: mixed
};
exports.createCoroutine = function<T>(children : mixed, handler : CoroutineHandler<T>, props : T, key : ?string = null) : ReactCoroutine {
var coroutine = {
// This tag allow us to uniquely identify this as a React Coroutine
$$typeof: REACT_COROUTINE_TYPE,
key: key == null ? null : '' + key,
children: children,
handler: handler,
props: props,
};
if (__DEV__) {
// TODO: Add _store property for marking this as validated.
if (Object.freeze) {
Object.freeze(coroutine.props);
Object.freeze(coroutine);
}
}
return coroutine;
};
exports.createYield = function(props : mixed, continuation : mixed, key : ?string = null) {
var yieldNode = {
// This tag allow us to uniquely identify this as a React Yield
$$typeof: REACT_YIELD_TYPE,
key: key == null ? null : '' + key,
props: props,
continuation: continuation,
};
if (__DEV__) {
// TODO: Add _store property for marking this as validated.
if (Object.freeze) {
Object.freeze(yieldNode.props);
Object.freeze(yieldNode);
}
}
return yieldNode;
};
/**
* Verifies the object is a coroutine object.
*/
exports.isCoroutine = function(object : mixed) : boolean {
return (
typeof object === 'object' &&
object !== null &&
object.$$typeof === REACT_COROUTINE_TYPE
);
};
/**
* Verifies the object is a yield object.
*/
exports.isYield = function(object : mixed) : boolean {
return (
typeof object === 'object' &&
object !== null &&
object.$$typeof === REACT_YIELD_TYPE
);
};
exports.REACT_YIELD_TYPE = REACT_YIELD_TYPE;
exports.REACT_COROUTINE_TYPE = REACT_COROUTINE_TYPE;
@@ -0,0 +1,25 @@
/**
* Copyright 2014-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.
*
* @providesModule ReactTypes
* @flow
*/
'use strict';
import type { ReactCoroutine, ReactYield } from 'ReactCoroutine';
export type ReactNode = ReactElement | ReactCoroutine | ReactYield | ReactText | ReactFragment;
export type ReactFragment = ReactEmpty | Iterable<ReactNode>;
export type ReactNodeList = ReactEmpty | ReactNode;
export type ReactText = string | number;
export type ReactEmpty = null | void | boolean;
@@ -280,14 +280,8 @@ describe('ReactComponent', function() {
'or a class/function (for composite components) but got: null.'
);
var Z = {};
expect(() => ReactTestUtils.renderIntoDocument(<Z />)).toThrowError(
'Element type is invalid: expected a string (for built-in components) ' +
'or a class/function (for composite components) but got: object.'
);
// One warning for each element creation
expect(console.error.calls.count()).toBe(3);
expect(console.error.calls.count()).toBe(2);
});
});