Merge pull request #7344 from acdlite/fibersetstate

[Fiber] setState
This commit is contained in:
Sebastian Markbåge
2016-09-13 15:31:14 -07:00
committed by GitHub
10 changed files with 525 additions and 17 deletions
+41 -7
View File
@@ -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');
@@ -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);
@@ -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);
},
};
+14
View File
@@ -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 {
@@ -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.
@@ -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,
@@ -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;
@@ -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;
@@ -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,
@@ -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.
@@ -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);
@@ -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);
}
@@ -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>) {
@@ -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;
}
@@ -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.
@@ -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
@@ -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);
@@ -29,9 +29,19 @@ var {
var timeHeuristicForUnitOfWork = 1;
module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
export type Scheduler = {
scheduleLowPriWork: (root : FiberRoot, priority : PriorityLevel) => void
};
const { beginWork } = ReactFiberBeginWork(config);
module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
// Use a closure to circumvent the circular dependency between the scheduler
// and ReactFiberBeginWork. Don't know if there's a better way to do this.
let scheduler;
function getScheduler(): Scheduler {
return scheduler;
}
const { beginWork } = ReactFiberBeginWork(config, getScheduler);
const { completeWork } = ReactFiberCompleteWork(config);
const { commitWork } = ReactFiberCommitWork(config);
@@ -133,6 +143,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
// The work is now done. We don't need this anymore. This flags
// to the system not to redo any work here.
workInProgress.pendingProps = null;
workInProgress.updateQueue = null;
const returnFiber = workInProgress.return;
@@ -259,7 +270,8 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
}
*/
return {
scheduler = {
scheduleLowPriWork: scheduleLowPriWork,
};
return scheduler;
};
@@ -0,0 +1,89 @@
/**
* 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 ReactFiberUpdateQueue
* @flow
*/
'use strict';
type UpdateQueueNode = {
partialState: any,
callback: ?Function,
callbackWasCalled: boolean,
next: ?UpdateQueueNode,
};
export type UpdateQueue = UpdateQueueNode & {
isReplace: boolean,
isForced: boolean,
tail: UpdateQueueNode
};
exports.createUpdateQueue = function(partialState : mixed) : UpdateQueue {
const queue = {
partialState,
callback: null,
callbackWasCalled: false,
next: null,
isReplace: false,
isForced: false,
tail: (null : any),
};
queue.tail = queue;
return queue;
};
exports.addToQueue = function(queue : UpdateQueue, partialState : mixed) : UpdateQueue {
const node = {
partialState,
callback: null,
callbackWasCalled: false,
next: null,
};
queue.tail.next = node;
queue.tail = node;
return queue;
};
exports.addCallbackToQueue = function(queue : UpdateQueue, callback: Function) : UpdateQueue {
if (queue.tail.callback) {
// If the tail already as a callback, add an empty node to queue
exports.addToQueue(queue, null);
}
queue.tail.callback = callback;
return queue;
};
exports.callCallbacks = function(queue : UpdateQueue, context : any) {
let node : ?UpdateQueueNode = queue;
while (node) {
if (node.callback && !node.callbackWasCalled) {
node.callbackWasCalled = true;
node.callback.call(context);
}
node = node.next;
}
};
exports.mergeUpdateQueue = function(queue : UpdateQueue, prevState : any, props : any) : any {
let node : ?UpdateQueueNode = queue;
let state = queue.isReplace ? null : Object.assign({}, prevState);
while (node) {
let partialState;
if (typeof node.partialState === 'function') {
const updateFn = node.partialState;
partialState = updateFn(state, props);
} else {
partialState = node.partialState;
}
state = Object.assign(state || {}, partialState);
node = node.next;
}
return state;
};
@@ -556,4 +556,208 @@ describe('ReactIncremental', () => {
expect(ops).toEqual(['Content', 'Bar', 'Middle']);
});
it('can update in the middle of a tree using setState', () => {
let instance;
class Bar extends React.Component {
constructor() {
super();
this.state = { a: 'a' };
instance = this;
}
render() {
return <div>{this.props.children}</div>;
}
}
function Foo() {
return (
<div>
<Bar />
</div>
);
}
ReactNoop.render(<Foo />);
ReactNoop.flush();
expect(instance.state).toEqual({ a: 'a' });
instance.setState({ b: 'b' });
ReactNoop.flush();
expect(instance.state).toEqual({ a: 'a', b: 'b' });
});
it('can queue multiple state updates', () => {
let instance;
class Bar extends React.Component {
constructor() {
super();
this.state = { a: 'a' };
instance = this;
}
render() {
return <div>{this.props.children}</div>;
}
}
function Foo() {
return (
<div>
<Bar />
</div>
);
}
ReactNoop.render(<Foo />);
ReactNoop.flush();
// Call setState multiple times before flushing
instance.setState({ b: 'b' });
instance.setState({ c: 'c' });
instance.setState({ d: 'd' });
ReactNoop.flush();
expect(instance.state).toEqual({ a: 'a', b: 'b', c: 'c', d: 'd' });
});
it('can use updater form of setState', () => {
let instance;
class Bar extends React.Component {
constructor() {
super();
this.state = { num: 1 };
instance = this;
}
render() {
return <div>{this.props.children}</div>;
}
}
function Foo({ multiplier }) {
return (
<div>
<Bar multiplier={multiplier} />
</div>
);
}
function updater(state, props) {
return { num: state.num * props.multiplier };
}
ReactNoop.render(<Foo multiplier={2} />);
ReactNoop.flush();
expect(instance.state.num).toEqual(1);
instance.setState(updater);
ReactNoop.flush();
expect(instance.state.num).toEqual(2);
instance.setState(updater);
ReactNoop.render(<Foo multiplier={3} />);
ReactNoop.flush();
expect(instance.state.num).toEqual(6);
});
it('can call setState inside update callback', () => {
let instance;
class Bar extends React.Component {
constructor() {
super();
this.state = { num: 1 };
instance = this;
}
render() {
return <div>{this.props.children}</div>;
}
}
function Foo({ multiplier }) {
return (
<div>
<Bar multiplier={multiplier} />
</div>
);
}
function updater(state, props) {
return { num: state.num * props.multiplier };
}
function callback() {
this.setState({ called: true });
}
ReactNoop.render(<Foo multiplier={2} />);
ReactNoop.flush();
instance.setState(updater);
instance.setState(updater, callback);
ReactNoop.flush();
expect(instance.state.num).toEqual(4);
expect(instance.state.called).toEqual(true);
});
it('can replaceState', () => {
let instance;
const Bar = React.createClass({
getInitialState() {
instance = this;
return { a: 'a' };
},
render() {
return <div>{this.props.children}</div>;
},
});
function Foo() {
return (
<div>
<Bar />
</div>
);
}
ReactNoop.render(<Foo />);
ReactNoop.flush();
instance.setState({ b: 'b' });
instance.setState({ c: 'c' });
instance.replaceState({ d: 'd' });
ReactNoop.flush();
expect(instance.state).toEqual({ d: 'd' });
});
it('can forceUpdate', () => {
const ops = [];
function Baz() {
ops.push('Baz');
return <div />;
}
let instance;
class Bar extends React.Component {
constructor() {
super();
instance = this;
}
shouldComponentUpdate() {
return false;
}
render() {
ops.push('Bar');
return <Baz />;
}
}
function Foo() {
ops.push('Foo');
return (
<div>
<Bar />
</div>
);
}
ReactNoop.render(<Foo />);
ReactNoop.flush();
expect(ops).toEqual(['Foo', 'Bar', 'Baz']);
instance.forceUpdate();
ReactNoop.flush();
expect(ops).toEqual(['Foo', 'Bar', 'Baz', 'Bar', 'Baz']);
});
});
@@ -354,4 +354,34 @@ describe('ReactIncrementalSideEffects', () => {
// moves to "current" without flushing due to having lower priority. Does this
// even happen? Maybe a child doesn't get processed because it is lower prio?
it('calls callback after update is flushed', () => {
let instance;
class Foo extends React.Component {
constructor() {
super();
instance = this;
this.state = { text: 'foo' };
}
render() {
return <span prop={this.state.text} />;
}
}
ReactNoop.render(<Foo />);
ReactNoop.flush();
expect(ReactNoop.root.children).toEqual([
span('foo'),
]);
let called = false;
instance.setState({ text: 'bar' }, () => {
expect(ReactNoop.root.children).toEqual([
span('bar'),
]);
called = true;
});
ReactNoop.flush();
expect(called).toBe(true);
});
// TODO: Test that callbacks are not lost if an update is preempted.
});