Add more life-cycles to Fiber

This refactors the initialization process so that we can share
it with the "module pattern" initialization.

There are a few new interesting scenarios unlocked by this.
E.g. constructor -> componentWillMount -> shouldComponentUpdate
-> componentDidMount when a render is aborted and resumed.
If shouldComponentUpdate returns true then we create a new
instance instead of trying componentWillMount again or
componentWillReceiveProps without being mounted.

Another strange thing is that the "previous props and state"
during componentWillReceiveProps, shouldComponentUpdate and
componentWillUpdate are all the previous render attempt. However,
componentDidMount's previous is the props/state at the previous
commit. That is because the first three can execute multiple
times before a didMount.
This commit is contained in:
Sebastian Markbage
2016-10-21 11:57:24 -07:00
parent a6728f961c
commit a87ec42f2a
2 changed files with 171 additions and 62 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 = 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 = workInProgress.memoizedProps;
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) &&
workInProgress.memoizedProps !== 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,
};
};