Skip to content

Commit

Permalink
Merge pull request #7344 from acdlite/fibersetstate
Browse files Browse the repository at this point in the history
[Fiber] setState
(cherry picked from commit 3e54b28)
  • Loading branch information
sebmarkbage authored and zpao committed Oct 4, 2016
1 parent 3bc9d9c commit da9a658
Show file tree
Hide file tree
Showing 10 changed files with 524 additions and 16 deletions.
48 changes: 41 additions & 7 deletions src/renderers/noop/ReactNoop.js
Expand Up @@ -20,6 +20,7 @@
'use strict';

import type { Fiber } from 'ReactFiber';
import type { UpdateQueue } from 'ReactFiberUpdateQueue';
import type { HostChildren } from 'ReactFiberReconciler';

var ReactFiberReconciler = require('ReactFiberReconciler');
Expand Down Expand Up @@ -153,30 +154,61 @@ var ReactNoop = {
return;
}

var bufferedLog = [];
function log(...args) {
bufferedLog.push(...args, '\n');
}

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);
log(' '.repeat(depth) + '- ' + child.type + '#' + child.id);
logHostInstances(child.children, depth + 1);
}
}
function logContainer(container : Container, depth) {
console.log(' '.repeat(depth) + '- [root#' + container.rootID + ']');
log(' '.repeat(depth) + '- [root#' + container.rootID + ']');
logHostInstances(container.children, depth + 1);
}

function logUpdateQueue(updateQueue : UpdateQueue, depth) {
log(
' '.repeat(depth + 1) + 'QUEUED UPDATES',
updateQueue.isReplace ? 'is replace' : '',
updateQueue.isForced ? 'is forced' : ''
);
log(
' '.repeat(depth + 1) + '~',
updateQueue.partialState,
updateQueue.callback ? 'with callback' : ''
);
var next;
while (next = updateQueue.next) {
log(
' '.repeat(depth + 1) + '~',
next.partialState,
next.callback ? 'with callback' : ''
);
}
}

function logFiber(fiber : Fiber, depth) {
console.log(
log(
' '.repeat(depth) + '- ' + (fiber.type ? fiber.type.name || fiber.type : '[root]'),
'[' + fiber.pendingWorkPriority + (fiber.pendingProps ? '*' : '') + ']'
);
if (fiber.updateQueue) {
logUpdateQueue(fiber.updateQueue, depth);
}
const childInProgress = fiber.progressedChild;
if (childInProgress && childInProgress !== fiber.child) {
console.log(' '.repeat(depth + 1) + 'IN PROGRESS: ' + fiber.progressedPriority);
log(' '.repeat(depth + 1) + 'IN PROGRESS: ' + fiber.progressedPriority);
logFiber(childInProgress, depth + 1);
if (fiber.child) {
console.log(' '.repeat(depth + 1) + 'CURRENT');
log(' '.repeat(depth + 1) + 'CURRENT');
}
} else if (fiber.child && fiber.updateQueue) {
log(' '.repeat(depth + 1) + 'CHILDREN');
}
if (fiber.child) {
logFiber(fiber.child, depth + 1);
Expand All @@ -186,10 +218,12 @@ var ReactNoop = {
}
}

console.log('HOST INSTANCES:');
log('HOST INSTANCES:');
logContainer(rootContainer, 0);
console.log('FIBERS:');
log('FIBERS:');
logFiber((root.stateNode : any).current, 0);

console.log(...bufferedLog);
},

};
Expand Down
14 changes: 14 additions & 0 deletions src/renderers/shared/fiber/ReactFiber.js
Expand Up @@ -15,6 +15,7 @@
import type { ReactCoroutine, ReactYield } from 'ReactCoroutine';
import type { TypeOfWork } from 'ReactTypeOfWork';
import type { PriorityLevel } from 'ReactPriorityLevel';
import type { UpdateQueue } from 'ReactFiberUpdateQueue';

var ReactTypeOfWork = require('ReactTypeOfWork');
var {
Expand Down Expand Up @@ -76,6 +77,12 @@ export type Fiber = Instance & {
pendingProps: any, // This type will be more specific once we overload the tag.
// TODO: I think that there is a way to merge pendingProps and memoizedProps.
memoizedProps: any, // The props used to create the output.
// A queue of local state updates.
updateQueue: ?UpdateQueue,
// The state used to create the output. This is a full state object.
memoizedState: any,
// Linked list of callbacks to call after updates are committed.
callbackList: ?UpdateQueue,
// 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.
Expand Down Expand Up @@ -151,6 +158,9 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber {

pendingProps: null,
memoizedProps: null,
updateQueue: null,
memoizedState: null,
callbackList: null,
output: null,

nextEffect: null,
Expand Down Expand Up @@ -192,6 +202,8 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
alt.sibling = fiber.sibling; // This should always be overridden. TODO: null
alt.ref = fiber.ref;
alt.pendingProps = fiber.pendingProps; // TODO: Pass as argument.
alt.updateQueue = fiber.updateQueue;
alt.callbackList = fiber.callbackList;
alt.pendingWorkPriority = priorityLevel;

alt.child = fiber.child;
Expand All @@ -217,6 +229,8 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
// pendingProps is here for symmetry but is unnecessary in practice for now.
// TODO: Pass in the new pendingProps as an argument maybe?
alt.pendingProps = fiber.pendingProps;
alt.updateQueue = fiber.updateQueue;
alt.callbackList = fiber.callbackList;
alt.pendingWorkPriority = priorityLevel;

alt.memoizedProps = fiber.memoizedProps;
Expand Down
113 changes: 108 additions & 5 deletions src/renderers/shared/fiber/ReactFiberBeginWork.js
Expand Up @@ -14,14 +14,18 @@

import type { ReactCoroutine } from 'ReactCoroutine';
import type { Fiber } from 'ReactFiber';
import type { FiberRoot } from 'ReactFiberRoot';
import type { HostConfig } from 'ReactFiberReconciler';
import type { Scheduler } from 'ReactFiberScheduler';
import type { PriorityLevel } from 'ReactPriorityLevel';
import type { UpdateQueue } from 'ReactFiberUpdateQueue';

var {
reconcileChildFibers,
reconcileChildFibersInPlace,
cloneChildFibers,
} = require('ReactChildFiber');
var { LowPriority } = require('ReactPriorityLevel');
var ReactTypeOfWork = require('ReactTypeOfWork');
var {
IndeterminateComponent,
Expand All @@ -37,8 +41,15 @@ var {
NoWork,
OffscreenPriority,
} = require('ReactPriorityLevel');
var {
createUpdateQueue,
addToQueue,
addCallbackToQueue,
mergeUpdateQueue,
} = require('ReactFiberUpdateQueue');
var ReactInstanceMap = require('ReactInstanceMap');

module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getScheduler : () => Scheduler) {

function markChildAsProgressed(current, workInProgress, priorityLevel) {
// We now have clones. Let's store them as the currently progressed work.
Expand Down Expand Up @@ -105,25 +116,116 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
return workInProgress.child;
}

function scheduleUpdate(fiber: Fiber, updateQueue: UpdateQueue, priorityLevel : PriorityLevel): void {
const { scheduleLowPriWork } = getScheduler();
fiber.updateQueue = updateQueue;
// Schedule update on the alternate as well, since we don't know which tree
// is current.
if (fiber.alternate) {
fiber.alternate.updateQueue = updateQueue;
}
while (true) {
if (fiber.pendingWorkPriority === NoWork ||
fiber.pendingWorkPriority >= priorityLevel) {
fiber.pendingWorkPriority = priorityLevel;
}
if (fiber.alternate) {
if (fiber.alternate.pendingWorkPriority === NoWork ||
fiber.alternate.pendingWorkPriority >= priorityLevel) {
fiber.alternate.pendingWorkPriority = priorityLevel;
}
}
// Duck type root
if (fiber.stateNode && fiber.stateNode.containerInfo) {
const root : FiberRoot = (fiber.stateNode : any);
scheduleLowPriWork(root, priorityLevel);
return;
}
if (!fiber.return) {
throw new Error('No root!');
}
fiber = fiber.return;
}
}

// Class component state updater
const updater = {
enqueueSetState(instance, partialState) {
const fiber = ReactInstanceMap.get(instance);
const updateQueue = fiber.updateQueue ?
addToQueue(fiber.updateQueue, partialState) :
createUpdateQueue(partialState);
scheduleUpdate(fiber, updateQueue, LowPriority);
},
enqueueReplaceState(instance, state) {
const fiber = ReactInstanceMap.get(instance);
const updateQueue = createUpdateQueue(state);
updateQueue.isReplace = true;
scheduleUpdate(fiber, updateQueue, LowPriority);
},
enqueueForceUpdate(instance) {
const fiber = ReactInstanceMap.get(instance);
const updateQueue = fiber.updateQueue || createUpdateQueue(null);
updateQueue.isForced = true;
scheduleUpdate(fiber, updateQueue, LowPriority);
},
enqueueCallback(instance, callback) {
const fiber = ReactInstanceMap.get(instance);
let updateQueue = fiber.updateQueue ?
fiber.updateQueue :
createUpdateQueue(null);
addCallbackToQueue(updateQueue, callback);
fiber.updateQueue = updateQueue;
if (fiber.alternate) {
fiber.alternate.updateQueue = updateQueue;
}
},
};

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 && current) {
props = current.memoizedProps;
}
// Compute the state using the memoized state and the update queue.
var updateQueue = workInProgress.updateQueue;
var previousState = current ? current.memoizedState : null;
var state = updateQueue ?
mergeUpdateQueue(updateQueue, previousState, props) :
previousState;

var instance = workInProgress.stateNode;
if (!instance) {
var ctor = workInProgress.type;
workInProgress.stateNode = instance = new ctor(props);
} else if (typeof instance.shouldComponentUpdate === 'function') {
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);
}
// The instance needs access to the fiber so that it can schedule updates
ReactInstanceMap.set(instance, workInProgress);
instance.updater = updater;
} 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;
if (!instance.shouldComponentUpdate(props)) {
instance.state = workInProgress.memoizedState;
if (!instance.shouldComponentUpdate(props, state)) {
return bailoutOnAlreadyFinishedWork(current, workInProgress);
}
}
}

instance.props = props;
instance.state = state;
var nextChildren = instance.render();
reconcileChildren(current, workInProgress, nextChildren);

Expand Down Expand Up @@ -251,10 +353,11 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
workInProgress.child = workInProgress.progressedChild;
}

if (workInProgress.pendingProps === null || (
if ((workInProgress.pendingProps === null || (
workInProgress.memoizedProps !== null &&
workInProgress.pendingProps === workInProgress.memoizedProps
)) {
)) &&
workInProgress.updateQueue === null) {
return bailoutOnAlreadyFinishedWork(current, workInProgress);
}

Expand Down
13 changes: 13 additions & 0 deletions src/renderers/shared/fiber/ReactFiberCommitWork.js
Expand Up @@ -22,6 +22,7 @@ var {
HostContainer,
HostComponent,
} = ReactTypeOfWork;
var { callCallbacks } = require('ReactFiberUpdateQueue');

module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {

Expand All @@ -31,6 +32,18 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
function commitWork(current : ?Fiber, finishedWork : Fiber) : void {
switch (finishedWork.tag) {
case ClassComponent: {
// Clear updates from current fiber. This must go before the callbacks
// are reset, in case an update is triggered from inside a callback. Is
// this safe? Relies on the assumption that work is only committed if
// the update queue is empty.
if (finishedWork.alternate) {
finishedWork.alternate.updateQueue = null;
}
if (finishedWork.callbackList) {
const { callbackList } = finishedWork;
finishedWork.callbackList = null;
callCallbacks(callbackList, finishedWork.stateNode);
}
// TODO: Fire componentDidMount/componentDidUpdate, update refs
return;
}
Expand Down
13 changes: 11 additions & 2 deletions src/renderers/shared/fiber/ReactFiberCompleteWork.js
Expand Up @@ -46,7 +46,6 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
}
}

/*
// TODO: It's possible this will create layout thrash issues because mutations
// of the DOM and life-cycles are interleaved. E.g. if a componentDidMount
// of a sibling reads, then the next sibling updates and reads etc.
Expand All @@ -59,7 +58,6 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
}
workInProgress.lastEffect = workInProgress;
}
*/

function transferOutput(child : ?Fiber, returnFiber : Fiber) {
// If we have a single result, we just pass that through as the output to
Expand Down Expand Up @@ -132,6 +130,17 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
return null;
case ClassComponent:
transferOutput(workInProgress.child, workInProgress);
// Don't use the state queue to compute the memoized state. We already
// merged it and assigned it to the instance. Transfer it from there.
// Also need to transfer the props, because pendingProps will be null
// in the case of an update
const { state, props } = workInProgress.stateNode;
workInProgress.memoizedState = state;
workInProgress.memoizedProps = props;
// Transfer update queue to callbackList field so callbacks can be
// called during commit phase.
workInProgress.callbackList = workInProgress.updateQueue;
markForPostEffect(workInProgress);
return null;
case HostContainer:
transferOutput(workInProgress.child, workInProgress);
Expand Down

0 comments on commit da9a658

Please sign in to comment.