Merge pull request #8015 from sebmarkbage/fiberclassinit

[Fiber] Add more life-cycles
This commit is contained in:
Sebastian Markbåge
2016-10-21 14:57:51 -04:00
committed by GitHub
5 changed files with 491 additions and 76 deletions
@@ -40,9 +40,6 @@ var {
NoWork,
OffscreenPriority,
} = require('ReactPriorityLevel');
var {
mergeUpdateQueue,
} = require('ReactFiberUpdateQueue');
var {
Placement,
} = require('ReactTypeOfSideEffect');
@@ -51,7 +48,11 @@ var ReactFiberClassComponent = require('ReactFiberClassComponent');
module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>, scheduleUpdate : (fiber: Fiber, priorityLevel : PriorityLevel) => void) {
const {
mount,
adoptClassInstance,
constructClassInstance,
mountClassInstance,
resumeMountClassInstance,
updateClassInstance,
} = ReactFiberClassComponent(scheduleUpdate);
function markChildAsProgressed(current, workInProgress, priorityLevel) {
@@ -156,54 +157,27 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>, s
}
function updateClassComponent(current : ?Fiber, workInProgress : Fiber) {
// A class component update is the result of either new props or new state.
// Account for the possibly of missing pending props by falling back to the
// memoized props.
var props = workInProgress.pendingProps;
if (!props) {
// If there isn't any new props, then we'll reuse the memoized props.
// This could be from already completed work.
props = workInProgress.memoizedProps;
if (!props) {
throw new Error('There should always be pending or memoized props.');
let shouldUpdate;
if (!current) {
if (!workInProgress.stateNode) {
// In the initial pass we might need to construct the instance.
constructClassInstance(workInProgress);
mountClassInstance(workInProgress);
shouldUpdate = true;
} else {
// In a resume, we'll already have an instance we can reuse.
shouldUpdate = resumeMountClassInstance(workInProgress);
}
}
// Compute the state using the memoized state and the update queue.
var updateQueue = workInProgress.updateQueue;
var previousState = workInProgress.memoizedState;
var state;
if (updateQueue) {
state = mergeUpdateQueue(updateQueue, previousState, props);
} else {
state = previousState;
shouldUpdate = updateClassInstance(current, workInProgress);
}
var instance = workInProgress.stateNode;
if (!instance) {
var ctor = workInProgress.type;
workInProgress.stateNode = instance = new ctor(props);
mount(workInProgress, instance);
state = instance.state || null;
} else if (typeof instance.shouldComponentUpdate === 'function' &&
!(updateQueue && updateQueue.isForced)) {
if (workInProgress.memoizedProps !== null) {
// Reset the props, in case this is a ping-pong case rather than a
// completed update case. For the completed update case, the instance
// props will already be the memoizedProps.
instance.props = workInProgress.memoizedProps;
instance.state = workInProgress.memoizedState;
if (!instance.shouldComponentUpdate(props, state)) {
return bailoutOnAlreadyFinishedWork(current, workInProgress);
}
}
if (!shouldUpdate) {
return bailoutOnAlreadyFinishedWork(current, workInProgress);
}
instance.props = props;
instance.state = state;
var nextChildren = instance.render();
// Rerender
const instance = workInProgress.stateNode;
const nextChildren = instance.render();
reconcileChildren(current, workInProgress, nextChildren);
return workInProgress.child;
}
@@ -258,22 +232,21 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>, s
}
function mountIndeterminateComponent(current, workInProgress) {
if (current) {
throw new Error('An indeterminate component should never have mounted.');
}
var fn = workInProgress.type;
var props = workInProgress.pendingProps;
var value = fn(props);
if (typeof value === 'object' && value && typeof value.render === 'function') {
// Proceed under the assumption that this is a class instance
workInProgress.tag = ClassComponent;
if (current) {
current.tag = ClassComponent;
}
adoptClassInstance(workInProgress, value);
mountClassInstance(workInProgress);
value = value.render();
} else {
// Proceed under the assumption that this is a functional component
workInProgress.tag = FunctionalComponent;
if (current) {
current.tag = FunctionalComponent;
}
}
reconcileChildren(current, workInProgress, value);
return workInProgress.child;
@@ -21,6 +21,7 @@ var {
createUpdateQueue,
addToQueue,
addCallbackToQueue,
mergeUpdateQueue,
} = require('ReactFiberUpdateQueue');
var ReactInstanceMap = require('ReactInstanceMap');
@@ -70,20 +71,155 @@ module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : Priori
},
};
function mount(workInProgress : Fiber, instance : any) {
const state = instance.state || null;
// The initial state must be added to the update queue in case
// setState is called before the initial render.
if (state !== null) {
workInProgress.updateQueue = createUpdateQueue(state);
}
function adoptClassInstance(workInProgress : Fiber, instance : any) : void {
instance.updater = updater;
workInProgress.stateNode = instance;
// The instance needs access to the fiber so that it can schedule updates
ReactInstanceMap.set(instance, workInProgress);
instance.updater = updater;
}
function constructClassInstance(workInProgress : Fiber) : any {
const ctor = workInProgress.type;
const props = workInProgress.pendingProps;
const instance = new ctor(props);
adoptClassInstance(workInProgress, instance);
return instance;
}
// Invokes the mount life-cycles on a previously never rendered instance.
function mountClassInstance(workInProgress : Fiber) : void {
const instance = workInProgress.stateNode;
const state = instance.state || null;
// A class component update is the result of either new props or new state.
// Account for the possibly of missing pending props by falling back to the
// memoized props.
let props = workInProgress.pendingProps;
if (!props) {
throw new Error('There must be pending props for an initial mount.');
}
instance.props = props;
instance.state = state;
if (typeof instance.componentWillMount === 'function') {
instance.componentWillMount();
// If we had additional state updates during this life-cycle, let's
// process them now.
const updateQueue = workInProgress.updateQueue;
if (updateQueue) {
instance.state = mergeUpdateQueue(updateQueue, state, props);
}
}
}
// Called on a preexisting class instance. Returns false if a resumed render
// could be reused.
function resumeMountClassInstance(workInProgress : Fiber) : boolean {
const instance = workInProgress.stateNode;
let newState = workInProgress.memoizedState;
let newProps = workInProgress.pendingProps;
if (!newProps) {
// If there isn't any new props, then we'll reuse the memoized props.
// This could be from already completed work.
newProps = workInProgress.memoizedProps;
if (!newProps) {
throw new Error('There should always be pending or memoized props.');
}
}
// TODO: Should we deal with a setState that happened after the last
// componentWillMount and before this componentWillMount? Probably
// unsupported anyway.
const updateQueue = workInProgress.updateQueue;
// If this completed, we might be able to just reuse this instance.
if (typeof instance.shouldComponentUpdate === 'function' &&
!(updateQueue && updateQueue.isForced) &&
workInProgress.memoizedProps !== null &&
!instance.shouldComponentUpdate(newProps, newState)) {
return false;
}
// If we didn't bail out we need to construct a new instance. We don't
// want to reuse one that failed to fully mount.
const newInstance = constructClassInstance(workInProgress);
newInstance.props = newProps;
newInstance.state = newState = newInstance.state || null;
if (typeof newInstance.componentWillMount === 'function') {
newInstance.componentWillMount();
// If we had additional state updates during this life-cycle, let's
// process them now.
const newUpdateQueue = workInProgress.updateQueue;
if (newUpdateQueue) {
instance.state = mergeUpdateQueue(newUpdateQueue, newState, newProps);
}
}
return true;
}
// Invokes the update life-cycles and returns false if it shouldn't rerender.
function updateClassInstance(current : Fiber, workInProgress : Fiber) : boolean {
const instance = workInProgress.stateNode;
const oldProps = workInProgress.memoizedProps || current.memoizedProps;
let newProps = workInProgress.pendingProps;
if (!newProps) {
// If there aren't any new props, then we'll reuse the memoized props.
// This could be from already completed work.
newProps = oldProps;
if (!newProps) {
throw new Error('There should always be pending or memoized props.');
}
}
// Note: During these life-cycles, instance.props/instance.state are what
// ever the previously attempted to render - not the "current". However,
// during componentDidUpdate we pass the "current" props.
if (oldProps !== newProps) {
if (typeof instance.componentWillReceiveProps === 'function') {
instance.componentWillReceiveProps(newProps);
}
}
// Compute the next state using the memoized state and the update queue.
const updateQueue = workInProgress.updateQueue;
const previousState = workInProgress.memoizedState;
// TODO: Previous state can be null.
let newState;
if (updateQueue) {
newState = mergeUpdateQueue(updateQueue, previousState, newProps);
} else {
newState = previousState;
}
if (typeof instance.shouldComponentUpdate === 'function' &&
!(updateQueue && updateQueue.isForced) &&
oldProps !== null &&
!instance.shouldComponentUpdate(newProps, newState)) {
// TODO: Should this get the new props/state updated regardless?
return false;
}
if (typeof instance.componentWillUpdate === 'function') {
instance.componentWillUpdate(newProps, newState);
}
instance.props = newProps;
instance.state = newState;
return true;
}
return {
mount,
adoptClassInstance,
constructClassInstance,
mountClassInstance,
resumeMountClassInstance,
updateClassInstance,
};
};
@@ -79,14 +79,13 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
nextScheduledRoot = nextScheduledRoot.nextScheduledRoot;
}
// TODO: This is scanning one root at a time. It should be scanning all
// roots for high priority work before moving on to lower priorities.
let root = nextScheduledRoot;
let highestPriorityRoot = null;
let highestPriorityLevel = NoWork;
while (root) {
if (highestPriorityLevel === NoWork ||
highestPriorityLevel > root.current.pendingWorkPriority) {
if (root.current.pendingWorkPriority !== NoWork && (
highestPriorityLevel === NoWork ||
highestPriorityLevel > root.current.pendingWorkPriority)) {
highestPriorityLevel = root.current.pendingWorkPriority;
highestPriorityRoot = root;
}
@@ -625,10 +625,10 @@ describe('ReactIncremental', () => {
// Normally shouldComponentUpdate->false is not enough to determine that we
// can safely reuse the old props, but I think in this case it would be ok,
// since it is a resume of already started work.
// Because of the above we can also not reuse the work of Bar because the
// Because of the above we can not reuse the work of Bar because the
// rerender of Content will generate a new element which will mean we don't
// auto-bail out from Bar.
expect(ops).toEqual(['Content', 'Bar', 'Middle']);
expect(ops).toEqual(['Bar', 'Middle']);
});
@@ -839,8 +839,14 @@ describe('ReactIncremental', () => {
it('can call sCU while resuming a partly mounted component', () => {
var ops = [];
var instances = new Set();
class Bar extends React.Component {
state = { y: 'A' };
constructor() {
super();
instances.add(this);
}
shouldComponentUpdate(newProps, newState) {
return this.props.x !== newProps.x ||
this.state.y !== newState.y;
@@ -855,20 +861,29 @@ describe('ReactIncremental', () => {
ops.push('Foo');
return [
<Bar key="a" x="A" />,
<Bar key="b" x="B" />,
<Bar key="b" x={props.step === 0 ? 'B' : 'B2'} />,
<Bar key="c" x="C" />,
<Bar key="d" x="D" />,
];
}
ReactNoop.render(<Foo />);
ReactNoop.flushDeferredPri(30);
expect(ops).toEqual(['Foo', 'Bar:A', 'Bar:B']);
ReactNoop.render(<Foo step={0} />);
ReactNoop.flushDeferredPri(40);
expect(ops).toEqual(['Foo', 'Bar:A', 'Bar:B', 'Bar:C']);
expect(instances.size).toBe(3);
ops = [];
ReactNoop.render(<Foo />);
ReactNoop.flushDeferredPri(40);
expect(ops).toEqual(['Foo', 'Bar:B', 'Bar:C']);
ReactNoop.render(<Foo step={1} />);
ReactNoop.flushDeferredPri(50);
// A completed and was reused. B completed but couldn't be reused because
// props differences. C didn't complete and therefore couldn't be reused.
// D never even started so it needed a new instance.
expect(ops).toEqual(['Foo', 'Bar:B2', 'Bar:C', 'Bar:D']);
// We expect each rerender to correspond to a new instance.
expect(instances.size).toBe(6);
});
it('gets new props when setting state on a partly updated component', () => {
@@ -927,4 +942,296 @@ describe('ReactIncremental', () => {
expect(ops).toEqual(['Bar:A-1', 'Baz', 'Baz']);
});
it('calls componentWillMount twice if the initial render is aborted', () => {
var ops = [];
class LifeCycle extends React.Component {
state = { x: this.props.x };
componentWillMount() {
ops.push('componentWillMount:' + this.state.x + '-' + this.props.x);
}
componentDidMount() {
ops.push('componentDidMount:' + this.state.x + '-' + this.props.x);
}
render() {
return <span />;
}
}
function Trail() {
ops.push('Trail');
}
function App(props) {
ops.push('App');
return (
<div>
<LifeCycle x={props.x} />
<Trail />
</div>
);
}
ReactNoop.render(<App x={0} />);
ReactNoop.flushDeferredPri(30);
expect(ops).toEqual([
'App',
'componentWillMount:0-0',
]);
ops = [];
ReactNoop.render(<App x={1} />);
ReactNoop.flush();
expect(ops).toEqual([
'App',
'componentWillMount:1-1',
'Trail',
'componentDidMount:1-1',
]);
});
it('calls componentWill* twice if an update render is aborted', () => {
var ops = [];
class LifeCycle extends React.Component {
componentWillMount() {
ops.push('componentWillMount:' + this.props.x);
}
componentDidMount() {
ops.push('componentDidMount:' + this.props.x);
}
componentWillReceiveProps(nextProps) {
ops.push('componentWillReceiveProps:' + this.props.x + '-' + nextProps.x);
}
shouldComponentUpdate(nextProps) {
ops.push('shouldComponentUpdate:' + this.props.x + '-' + nextProps.x);
return true;
}
componentWillUpdate(nextProps) {
ops.push('componentWillUpdate:' + this.props.x + '-' + nextProps.x);
}
componentDidUpdate(prevProps) {
ops.push('componentDidUpdate:' + this.props.x + '-' + prevProps.x);
}
render() {
ops.push('render:' + this.props.x);
return <span />;
}
}
function Sibling() {
// The sibling is used to confirm that we've completed the first child,
// but not yet flushed.
ops.push('Sibling');
return <span />;
}
function App(props) {
ops.push('App');
return [
<LifeCycle key="a" x={props.x} />,
<Sibling key="b" />,
];
}
ReactNoop.render(<App x={0} />);
ReactNoop.flush();
expect(ops).toEqual([
'App',
'componentWillMount:0',
'render:0',
'Sibling',
'componentDidMount:0',
]);
ops = [];
ReactNoop.render(<App x={1} />);
ReactNoop.flushDeferredPri(30);
expect(ops).toEqual([
'App',
'componentWillReceiveProps:0-1',
'shouldComponentUpdate:0-1',
'componentWillUpdate:0-1',
'render:1',
'Sibling',
// no componentDidUpdate
]);
ops = [];
ReactNoop.render(<App x={2} />);
ReactNoop.flush();
expect(ops).toEqual([
'App',
'componentWillReceiveProps:1-2',
'shouldComponentUpdate:1-2',
'componentWillUpdate:1-2',
'render:2',
'Sibling',
// When componentDidUpdate finally gets called, it covers both updates.
'componentDidUpdate:2-0',
]);
});
it('does not call componentWillReceiveProps for state-only updates', () => {
var ops = [];
var instances = [];
class LifeCycle extends React.Component {
state = { x: 0 };
tick() {
this.setState({
x: this.state.x + 1,
});
}
componentWillMount() {
instances.push(this);
ops.push('componentWillMount:' + this.state.x);
}
componentDidMount() {
ops.push('componentDidMount:' + this.state.x);
}
componentWillReceiveProps(nextProps) {
ops.push('componentWillReceiveProps');
}
shouldComponentUpdate(nextProps, nextState) {
ops.push('shouldComponentUpdate:' + this.state.x + '-' + nextState.x);
return true;
}
componentWillUpdate(nextProps, nextState) {
ops.push('componentWillUpdate:' + this.state.x + '-' + nextState.x);
}
componentDidUpdate(prevProps, prevState) {
ops.push('componentDidUpdate:' + this.state.x + '-' + prevState.x);
}
render() {
ops.push('render:' + this.state.x);
return <span />;
}
}
// This wrap is a bit contrived because we can't pause a completed root and
// there is currently an issue where a component can't reuse its render
// output unless it fully completed.
class Wrap extends React.Component {
state = { y: 0 };
componentWillMount() {
instances.push(this);
}
tick() {
this.setState({
y: this.state.y + 1,
});
}
render() {
ops.push('Wrap');
return <LifeCycle y={this.state.y} />;
}
}
function Sibling() {
// The sibling is used to confirm that we've completed the first child,
// but not yet flushed.
ops.push('Sibling');
return <span />;
}
function App(props) {
ops.push('App');
return [
<Wrap key="a" />,
<Sibling key="b" />,
];
}
ReactNoop.render(<App y={0} />);
ReactNoop.flush();
expect(ops).toEqual([
'App',
'Wrap',
'componentWillMount:0',
'render:0',
'Sibling',
'componentDidMount:0',
]);
ops = [];
// LifeCycle
instances[1].tick();
ReactNoop.flushDeferredPri(25);
expect(ops).toEqual([
// no componentWillReceiveProps
'shouldComponentUpdate:0-1',
'componentWillUpdate:0-1',
'render:1',
// no componentDidUpdate
]);
ops = [];
// LifeCycle
instances[1].tick();
ReactNoop.flush();
expect(ops).toEqual([
// no componentWillReceiveProps
'shouldComponentUpdate:1-2',
'componentWillUpdate:1-2',
'render:2',
// When componentDidUpdate finally gets called, it covers both updates.
'componentDidUpdate:2-0',
]);
ops = [];
// Next we will update props of LifeCycle by updating its parent.
instances[0].tick();
ReactNoop.flushDeferredPri(30);
expect(ops).toEqual([
'Wrap',
'componentWillReceiveProps',
'shouldComponentUpdate:2-2',
'componentWillUpdate:2-2',
'render:2',
// no componentDidUpdate
]);
ops = [];
// Next we will update LifeCycle directly but not with new props.
instances[1].tick();
ReactNoop.flush();
expect(ops).toEqual([
// This should not trigger another componentWillReceiveProps because
// we never got new props.
'shouldComponentUpdate:2-3',
'componentWillUpdate:2-3',
'render:3',
'componentDidUpdate:3-2',
]);
// TODO: Test that we get the expected values for the same scenario with
// incomplete parents.
});
});
@@ -598,7 +598,7 @@ describe('ReactIncrementalSideEffects', () => {
),
]);
expect(ops).toEqual(['Baz', 'Bar', 'Baz', 'Bar', 'Bar']);
expect(ops).toEqual(['Bar', 'Baz', 'Bar', 'Bar']);
});
it('deprioritizes setStates that happens within a deprioritized tree', () => {