Skip to content
48 changes: 41 additions & 7 deletions src/renderers/noop/ReactNoop.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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;
Copy link
Collaborator

@sebmarkbage sebmarkbage Aug 7, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend to prefer extract such lists off the object, into a variable, and reset it to null on the object before calling it. Makes it easier to reason about reentry that happen inside of the callbacks, such as new callbacks being scheduled onto this list or errors being thrown.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call

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
Original file line number Diff line number Diff line change
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
Loading