From 94d2317b4b9c5c8edb078bb2692e26969a19f0ae Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Sat, 23 Jul 2016 18:59:56 -0700 Subject: [PATCH 01/15] setState for Fiber Updates are scheduled by setting a work priority on the fiber and bubbling it to the root. Because the instance does not know which tree is current at any given time, the update is scheduled on both fiber alternates. Need to add more unit tests to cover edge cases. --- src/renderers/shared/fiber/ReactFiber.js | 8 +++ .../shared/fiber/ReactFiberBeginWork.js | 66 ++++++++++++++++++- .../shared/fiber/ReactFiberCompleteWork.js | 1 + .../shared/fiber/ReactFiberScheduler.js | 19 +++++- .../fiber/__tests__/ReactIncremental-test.js | 32 +++++++++ 5 files changed, 121 insertions(+), 5 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index 967c6f78b07..c2b8ea202a6 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -76,6 +76,10 @@ 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. + // Local state for class components. May need better naming to disambiguate + // from stateNode. + pendingState: any, + memoizedState: any, // The state used to create the output. // 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 +155,8 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber { pendingProps: null, memoizedProps: null, + pendingState: null, + memoizedState: null, output: null, nextEffect: null, @@ -192,6 +198,7 @@ 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.pendingState = fiber.pendingState; alt.pendingWorkPriority = priorityLevel; alt.child = fiber.child; @@ -217,6 +224,7 @@ 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.pendingState = fiber.pendingState; alt.pendingWorkPriority = priorityLevel; alt.memoizedProps = fiber.memoizedProps; diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index a21bd4059a2..f4c7049a0b5 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -14,7 +14,9 @@ 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'; var { @@ -22,6 +24,7 @@ var { reconcileChildFibersInPlace, cloneChildFibers, } = require('ReactChildFiber'); +var { LowPriority } = require('ReactPriorityLevel'); var ReactTypeOfWork = require('ReactTypeOfWork'); var { IndeterminateComponent, @@ -38,7 +41,7 @@ var { OffscreenPriority, } = require('ReactPriorityLevel'); -module.exports = function(config : HostConfig) { +module.exports = function(config : HostConfig, getScheduler: () => Scheduler) { function markChildAsProgressed(current, workInProgress, priorityLevel) { // We now have clones. Let's store them as the currently progressed work. @@ -105,25 +108,81 @@ module.exports = function(config : HostConfig) { return workInProgress.child; } + function updateFiber(fiber: Fiber, state: any, priorityLevel : PriorityLevel): void { + const { scheduleLowPriWork } = getScheduler(); + fiber.pendingState = state; + + while (true) { + if (fiber.pendingWorkPriority === NoWork || + fiber.pendingWorkPriority >= priorityLevel) { + fiber.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 = instance._fiber; + + const prevState = fiber.pendingState || fiber.memoizedState; + const state = Object.assign({}, prevState, partialState); + + // Must schedule an update on both alternates, because we don't know tree + // is current. + updateFiber(fiber, state, LowPriority); + if (fiber.alternate) { + updateFiber(fiber.alternate, state, LowPriority); + } + }, + }; + 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 or state by falling + // back to the most recent props or state. var props = workInProgress.pendingProps; + var state = workInProgress.pendingState; + if (!props && current) { + props = current.memoizedProps; + } + if (!state && current) { + state = current.memoizedState; + } + var instance = workInProgress.stateNode; if (!instance) { var ctor = workInProgress.type; workInProgress.stateNode = instance = new ctor(props); + state = workInProgress.pendingState = instance.state || null; + // The instance needs access to the fiber so that it can schedule updates + instance._fiber = workInProgress; + instance.updater = updater; } else if (typeof instance.shouldComponentUpdate === 'function') { 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); @@ -253,7 +312,8 @@ module.exports = function(config : HostConfig) { if (workInProgress.pendingProps === null || ( workInProgress.memoizedProps !== null && - workInProgress.pendingProps === workInProgress.memoizedProps + workInProgress.pendingProps === workInProgress.memoizedProps && + workInProgress.pendingState === workInProgress.memoizedState )) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index 14e95a0d735..350135e9c3b 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -67,6 +67,7 @@ module.exports = function(config : HostConfig) { // the linked list of fibers that has the individual output values. returnFiber.output = (child && !child.sibling) ? child.output : child; returnFiber.memoizedProps = returnFiber.pendingProps; + returnFiber.memoizedState = returnFiber.pendingState; } function recursivelyFillYields(yields, output : ?Fiber | ?ReifiedYield) { diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index e04c846f206..4f8b017a9d2 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -29,9 +29,19 @@ var { var timeHeuristicForUnitOfWork = 1; +export type Scheduler = { + scheduleLowPriWork: (root : FiberRoot, priority : PriorityLevel) => void +}; + module.exports = function(config : HostConfig) { + // 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); + const { beginWork } = ReactFiberBeginWork(config, getScheduler); const { completeWork } = ReactFiberCompleteWork(config); const { commitWork } = ReactFiberCommitWork(config); @@ -133,6 +143,10 @@ module.exports = function(config : HostConfig) { // 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.pendingState = null; + if (current) { + current.pendingState = null; + } const returnFiber = workInProgress.return; @@ -259,7 +273,8 @@ module.exports = function(config : HostConfig) { } */ - return { + scheduler = { scheduleLowPriWork: scheduleLowPriWork, }; + return scheduler; }; diff --git a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js index 608a95ea4ff..7c54db5667b 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js @@ -556,4 +556,36 @@ describe('ReactIncremental', () => { expect(ops).toEqual(['Content', 'Bar', 'Middle']); }); + + it('can update in the middle of a tree using setState', () => { + let instance; + let states = []; + + class Bar extends React.Component { + constructor() { + super(); + this.state = { string: 'a' }; + instance = this; + } + render() { + states.push(this.state.string); + return
{this.props.children}
; + } + } + + function Foo() { + return ( +
+ +
+ ); + } + + ReactNoop.render(); + ReactNoop.flush(); + expect(states).toEqual(['a']); + instance.setState({ string: 'b' }); + ReactNoop.flush(); + expect(states).toEqual(['a', 'b']); + }); }); From 8b200c5e7822b137029db0634094f69d790f2272 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Sun, 24 Jul 2016 22:49:47 -0700 Subject: [PATCH 02/15] Use queue for pendingState Changes the type of pendingState to be a linked list of partial state objects. --- src/renderers/shared/fiber/ReactFiber.js | 10 ++-- .../shared/fiber/ReactFiberBeginWork.js | 57 +++++++++++++------ .../shared/fiber/ReactFiberCompleteWork.js | 5 +- .../shared/fiber/ReactFiberPendingState.js | 38 +++++++++++++ .../fiber/__tests__/ReactIncremental-test.js | 40 +++++++++++-- 5 files changed, 122 insertions(+), 28 deletions(-) create mode 100644 src/renderers/shared/fiber/ReactFiberPendingState.js diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index c2b8ea202a6..e5ec0b6b091 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -15,6 +15,7 @@ import type { ReactCoroutine, ReactYield } from 'ReactCoroutine'; import type { TypeOfWork } from 'ReactTypeOfWork'; import type { PriorityLevel } from 'ReactPriorityLevel'; +import type { PendingState } from 'ReactFiberPendingState'; var ReactTypeOfWork = require('ReactTypeOfWork'); var { @@ -76,10 +77,11 @@ 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. - // Local state for class components. May need better naming to disambiguate - // from stateNode. - pendingState: any, - memoizedState: any, // The state used to create the output. + // Local state for class components. Either null or a linked list of partial + // state objects. + pendingState: PendingState, + // The state used to create the output. This is a full state object. + memoizedState: any, // 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. diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index f4c7049a0b5..fb061c60d38 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -18,6 +18,7 @@ import type { FiberRoot } from 'ReactFiberRoot'; import type { HostConfig } from 'ReactFiberReconciler'; import type { Scheduler } from 'ReactFiberScheduler'; import type { PriorityLevel } from 'ReactPriorityLevel'; +import type { PendingState } from 'ReactFiberPendingState'; var { reconcileChildFibers, @@ -40,6 +41,10 @@ var { NoWork, OffscreenPriority, } = require('ReactPriorityLevel'); +var { + createPendingState, + mergePendingState, +} = require('ReactFiberPendingState'); module.exports = function(config : HostConfig, getScheduler: () => Scheduler) { @@ -108,10 +113,9 @@ module.exports = function(config : HostConfig, getSchedu return workInProgress.child; } - function updateFiber(fiber: Fiber, state: any, priorityLevel : PriorityLevel): void { + function scheduleUpdate(fiber: Fiber, pendingState: PendingState, priorityLevel : PriorityLevel): void { const { scheduleLowPriWork } = getScheduler(); - fiber.pendingState = state; - + fiber.pendingState = pendingState; while (true) { if (fiber.pendingWorkPriority === NoWork || fiber.pendingWorkPriority >= priorityLevel) { @@ -134,37 +138,55 @@ module.exports = function(config : HostConfig, getSchedu const updater = { enqueueSetState(instance, partialState) { const fiber = instance._fiber; - - const prevState = fiber.pendingState || fiber.memoizedState; - const state = Object.assign({}, prevState, partialState); + let pendingState = fiber.pendingState; + + // Append to pending state queue + const nextPendingStateNode = createPendingState(partialState); + if (pendingState === null) { + pendingState = nextPendingStateNode; + } else { + if (pendingState.tail === null) { + pendingState.next = nextPendingStateNode; + } else { + pendingState.tail.next = nextPendingStateNode; + } + pendingState.tail = nextPendingStateNode; + } // Must schedule an update on both alternates, because we don't know tree // is current. - updateFiber(fiber, state, LowPriority); + scheduleUpdate(fiber, pendingState, LowPriority); if (fiber.alternate) { - updateFiber(fiber.alternate, state, LowPriority); + scheduleUpdate(fiber.alternate, pendingState, LowPriority); } }, }; 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 or state by falling - // back to the most recent props or state. + // Account for the possibly of missing pending props by falling back to the + // memoized props. var props = workInProgress.pendingProps; - var state = workInProgress.pendingState; if (!props && current) { props = current.memoizedProps; } - if (!state && current) { - state = current.memoizedState; + // Compute the state using the memoized state and the pending state queue. + var pendingState = workInProgress.pendingState; + var state; + if (!current) { + state = mergePendingState(null, pendingState); + } else { + state = mergePendingState(current.memoizedState, pendingState); } var instance = workInProgress.stateNode; if (!instance) { var ctor = workInProgress.type; workInProgress.stateNode = instance = new ctor(props); - state = workInProgress.pendingState = instance.state || null; + state = instance.state || null; + // The initial state must be added to the pending state queue in case + // setState is called before the initial render. + workInProgress.pendingState = createPendingState(state); // The instance needs access to the fiber so that it can schedule updates instance._fiber = workInProgress; instance.updater = updater; @@ -310,11 +332,12 @@ module.exports = function(config : HostConfig, getSchedu workInProgress.child = workInProgress.progressedChild; } - if (workInProgress.pendingProps === null || ( + if ((workInProgress.pendingProps === null || ( workInProgress.memoizedProps !== null && workInProgress.pendingProps === workInProgress.memoizedProps && - workInProgress.pendingState === workInProgress.memoizedState - )) { + )) && + workInProgress.pendingState === null + ) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index 350135e9c3b..10f0571186d 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -67,7 +67,6 @@ module.exports = function(config : HostConfig) { // the linked list of fibers that has the individual output values. returnFiber.output = (child && !child.sibling) ? child.output : child; returnFiber.memoizedProps = returnFiber.pendingProps; - returnFiber.memoizedState = returnFiber.pendingState; } function recursivelyFillYields(yields, output : ?Fiber | ?ReifiedYield) { @@ -133,6 +132,10 @@ module.exports = function(config : HostConfig) { return null; case ClassComponent: transferOutput(workInProgress.child, workInProgress); + // Don't use the pending state queue to compute the memoized state. We + // already merged it and assigned it to the instance. Copy it from there. + const state = workInProgress.stateNode.state; + workInProgress.memoizedState = state; return null; case HostContainer: transferOutput(workInProgress.child, workInProgress); diff --git a/src/renderers/shared/fiber/ReactFiberPendingState.js b/src/renderers/shared/fiber/ReactFiberPendingState.js new file mode 100644 index 00000000000..10c6615b7c7 --- /dev/null +++ b/src/renderers/shared/fiber/ReactFiberPendingState.js @@ -0,0 +1,38 @@ +/** + * 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 ReactFiberPendingState + * @flow + */ + +'use strict'; + +export type PendingState = { + partialState: any, + next: PendingState, + tail: PendingState +} | null; + +exports.createPendingState = function(partialState: mixed): PendingState { + return { + partialState, + next: null, + tail: null, + }; +}; + +exports.mergePendingState = function(prevState: any, queue: PendingState): any { + if (queue === null) { + return prevState; + } + let state = Object.assign({}, prevState, queue.partialState); + while (queue = queue.next) { + state = Object.assign(state, queue.partialState); + } + return state; +}; diff --git a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js index 7c54db5667b..c792e9e2a70 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js @@ -559,16 +559,42 @@ describe('ReactIncremental', () => { it('can update in the middle of a tree using setState', () => { let instance; - let states = []; + class Bar extends React.Component { + constructor() { + super(); + this.state = { a: 'a' }; + instance = this; + } + render() { + return
{this.props.children}
; + } + } + function Foo() { + return ( +
+ +
+ ); + } + + ReactNoop.render(); + 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 = { string: 'a' }; + this.state = { a: 'a' }; instance = this; } render() { - states.push(this.state.string); return
{this.props.children}
; } } @@ -583,9 +609,11 @@ describe('ReactIncremental', () => { ReactNoop.render(); ReactNoop.flush(); - expect(states).toEqual(['a']); - instance.setState({ string: 'b' }); + // Call setState multiple times before flushing + instance.setState({ b: 'b' }); + instance.setState({ c: 'c' }); + instance.setState({ d: 'd' }); ReactNoop.flush(); - expect(states).toEqual(['a', 'b']); + expect(instance.state).toEqual({ a: 'a', b: 'b', c: 'c', d: 'd' }); }); }); From 25d199abe01befe07bed0c7d1ae55af47857650c Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 25 Jul 2016 09:09:45 -0700 Subject: [PATCH 03/15] Updater form of setState Add support for setState((state, props) => newState). Rename pendingState to stateQueue. --- src/renderers/shared/fiber/ReactFiber.js | 13 +++-- .../shared/fiber/ReactFiberBeginWork.js | 45 ++++++++-------- .../shared/fiber/ReactFiberCompleteWork.js | 4 +- .../shared/fiber/ReactFiberPendingState.js | 38 -------------- .../shared/fiber/ReactFiberScheduler.js | 4 +- .../shared/fiber/ReactFiberStateQueue.js | 51 +++++++++++++++++++ .../fiber/__tests__/ReactIncremental-test.js | 37 ++++++++++++++ 7 files changed, 118 insertions(+), 74 deletions(-) delete mode 100644 src/renderers/shared/fiber/ReactFiberPendingState.js create mode 100644 src/renderers/shared/fiber/ReactFiberStateQueue.js diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index e5ec0b6b091..33128d85dd5 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -15,7 +15,7 @@ import type { ReactCoroutine, ReactYield } from 'ReactCoroutine'; import type { TypeOfWork } from 'ReactTypeOfWork'; import type { PriorityLevel } from 'ReactPriorityLevel'; -import type { PendingState } from 'ReactFiberPendingState'; +import type { StateQueue } from 'ReactFiberStateQueue'; var ReactTypeOfWork = require('ReactTypeOfWork'); var { @@ -77,9 +77,8 @@ 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. - // Local state for class components. Either null or a linked list of partial - // state objects. - pendingState: PendingState, + // A queue of local state updates. + stateQueue: StateQueue, // The state used to create the output. This is a full state object. memoizedState: any, // Output is the return value of this fiber, or a linked list of return values @@ -157,7 +156,7 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber { pendingProps: null, memoizedProps: null, - pendingState: null, + stateQueue: null, memoizedState: null, output: null, @@ -200,7 +199,7 @@ 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.pendingState = fiber.pendingState; + alt.stateQueue = fiber.stateQueue; alt.pendingWorkPriority = priorityLevel; alt.child = fiber.child; @@ -226,7 +225,7 @@ 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.pendingState = fiber.pendingState; + alt.stateQueue = fiber.stateQueue; alt.pendingWorkPriority = priorityLevel; alt.memoizedProps = fiber.memoizedProps; diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index fb061c60d38..ceecedb3221 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -18,7 +18,7 @@ import type { FiberRoot } from 'ReactFiberRoot'; import type { HostConfig } from 'ReactFiberReconciler'; import type { Scheduler } from 'ReactFiberScheduler'; import type { PriorityLevel } from 'ReactPriorityLevel'; -import type { PendingState } from 'ReactFiberPendingState'; +import type { StateQueue } from 'ReactFiberStateQueue'; var { reconcileChildFibers, @@ -42,11 +42,12 @@ var { OffscreenPriority, } = require('ReactPriorityLevel'); var { - createPendingState, - mergePendingState, -} = require('ReactFiberPendingState'); + createStateQueue, + addToQueue, + mergeStateQueue, +} = require('ReactFiberStateQueue'); -module.exports = function(config : HostConfig, getScheduler: () => Scheduler) { +module.exports = function(config : HostConfig, getScheduler : () => Scheduler) { function markChildAsProgressed(current, workInProgress, priorityLevel) { // We now have clones. Let's store them as the currently progressed work. @@ -113,9 +114,9 @@ module.exports = function(config : HostConfig, getSchedu return workInProgress.child; } - function scheduleUpdate(fiber: Fiber, pendingState: PendingState, priorityLevel : PriorityLevel): void { + function scheduleUpdate(fiber: Fiber, stateQueue: StateQueue, priorityLevel : PriorityLevel): void { const { scheduleLowPriWork } = getScheduler(); - fiber.pendingState = pendingState; + fiber.stateQueue = stateQueue; while (true) { if (fiber.pendingWorkPriority === NoWork || fiber.pendingWorkPriority >= priorityLevel) { @@ -138,26 +139,20 @@ module.exports = function(config : HostConfig, getSchedu const updater = { enqueueSetState(instance, partialState) { const fiber = instance._fiber; - let pendingState = fiber.pendingState; + let stateQueue = fiber.stateQueue; // Append to pending state queue - const nextPendingStateNode = createPendingState(partialState); - if (pendingState === null) { - pendingState = nextPendingStateNode; + if (stateQueue === null) { + stateQueue = createStateQueue(partialState); } else { - if (pendingState.tail === null) { - pendingState.next = nextPendingStateNode; - } else { - pendingState.tail.next = nextPendingStateNode; - } - pendingState.tail = nextPendingStateNode; + addToQueue(stateQueue, partialState); } // Must schedule an update on both alternates, because we don't know tree // is current. - scheduleUpdate(fiber, pendingState, LowPriority); + scheduleUpdate(fiber, stateQueue, LowPriority); if (fiber.alternate) { - scheduleUpdate(fiber.alternate, pendingState, LowPriority); + scheduleUpdate(fiber.alternate, stateQueue, LowPriority); } }, }; @@ -171,12 +166,12 @@ module.exports = function(config : HostConfig, getSchedu props = current.memoizedProps; } // Compute the state using the memoized state and the pending state queue. - var pendingState = workInProgress.pendingState; + var stateQueue = workInProgress.stateQueue; var state; if (!current) { - state = mergePendingState(null, pendingState); + state = mergeStateQueue(null, props, stateQueue); } else { - state = mergePendingState(current.memoizedState, pendingState); + state = mergeStateQueue(current.memoizedState, props, stateQueue); } var instance = workInProgress.stateNode; @@ -186,7 +181,7 @@ module.exports = function(config : HostConfig, getSchedu state = instance.state || null; // The initial state must be added to the pending state queue in case // setState is called before the initial render. - workInProgress.pendingState = createPendingState(state); + workInProgress.stateQueue = createStateQueue(state); // The instance needs access to the fiber so that it can schedule updates instance._fiber = workInProgress; instance.updater = updater; @@ -334,9 +329,9 @@ module.exports = function(config : HostConfig, getSchedu if ((workInProgress.pendingProps === null || ( workInProgress.memoizedProps !== null && - workInProgress.pendingProps === workInProgress.memoizedProps && + workInProgress.pendingProps === workInProgress.memoizedProps )) && - workInProgress.pendingState === null + workInProgress.stateQueue === null ) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index 10f0571186d..fdeb1f5e56c 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -132,8 +132,8 @@ module.exports = function(config : HostConfig) { return null; case ClassComponent: transferOutput(workInProgress.child, workInProgress); - // Don't use the pending state queue to compute the memoized state. We - // already merged it and assigned it to the instance. Copy it from there. + // Don't use the state queue to compute the memoized state. We already + // merged it and assigned it to the instance. Copy it from there. const state = workInProgress.stateNode.state; workInProgress.memoizedState = state; return null; diff --git a/src/renderers/shared/fiber/ReactFiberPendingState.js b/src/renderers/shared/fiber/ReactFiberPendingState.js deleted file mode 100644 index 10c6615b7c7..00000000000 --- a/src/renderers/shared/fiber/ReactFiberPendingState.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * 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 ReactFiberPendingState - * @flow - */ - -'use strict'; - -export type PendingState = { - partialState: any, - next: PendingState, - tail: PendingState -} | null; - -exports.createPendingState = function(partialState: mixed): PendingState { - return { - partialState, - next: null, - tail: null, - }; -}; - -exports.mergePendingState = function(prevState: any, queue: PendingState): any { - if (queue === null) { - return prevState; - } - let state = Object.assign({}, prevState, queue.partialState); - while (queue = queue.next) { - state = Object.assign(state, queue.partialState); - } - return state; -}; diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 4f8b017a9d2..2136907d1af 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -143,9 +143,9 @@ module.exports = function(config : HostConfig) { // 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.pendingState = null; + workInProgress.stateQueue = null; if (current) { - current.pendingState = null; + current.stateQueue = null; } const returnFiber = workInProgress.return; diff --git a/src/renderers/shared/fiber/ReactFiberStateQueue.js b/src/renderers/shared/fiber/ReactFiberStateQueue.js new file mode 100644 index 00000000000..e8d6c7385e2 --- /dev/null +++ b/src/renderers/shared/fiber/ReactFiberStateQueue.js @@ -0,0 +1,51 @@ +/** + * 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 ReactFiberStateQueue + * @flow + */ + +'use strict'; + +export type StateQueue = { + partialState: any, + next: StateQueue, + tail: StateQueue +} | null; + +exports.createStateQueue = function(partialState : mixed) : StateQueue { + return { + partialState, + next: null, + tail: null, + }; +}; + +exports.addToQueue = function(queue : StateQueue, partialState : mixed) { + const node = exports.createStateQueue(partialState); + if (queue.tail === null) { + queue.next = node; + } else { + queue.tail.next = node; + } + queue.tail = node; +} + +exports.mergeStateQueue = function(prevState : any, props : any, queue : StateQueue) : any { + if (queue === null) { + return prevState; + } + let state = Object.assign({}, prevState); + do { + const partialState = typeof queue.partialState === 'function' ? + queue.partialState(state, props) : + queue.partialState; + state = Object.assign(state, partialState); + } while (queue = queue.next); + return state; +}; diff --git a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js index c792e9e2a70..e7dccc7603c 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js @@ -616,4 +616,41 @@ describe('ReactIncremental', () => { 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
{this.props.children}
; + } + } + + function Foo({ multiplier }) { + return ( +
+ +
+ ); + } + + function updater(state, props) { + return { num: state.num * props.multiplier }; + } + + ReactNoop.render(); + ReactNoop.flush(); + expect(instance.state.num).toEqual(1); + instance.setState(updater); + ReactNoop.flush(); + expect(instance.state.num).toEqual(2); + ReactNoop.render(); + instance.setState(updater); + ReactNoop.flush(); + expect(instance.state.num).toEqual(6); + }); }); From 909caccebd7d17851b28d343169c2b5a6d2819f8 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 25 Jul 2016 09:27:59 -0700 Subject: [PATCH 04/15] Clean up Rather than bubble up both trees, bubble up once and assign to the alternate at each level. Extract logic for adding to the queue to the StateQueue module. --- .../shared/fiber/ReactFiberBeginWork.js | 28 +++++++++---------- .../shared/fiber/ReactFiberStateQueue.js | 8 ++++-- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index ceecedb3221..3004ce13d23 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -117,11 +117,24 @@ module.exports = function(config : HostConfig, getSchedu function scheduleUpdate(fiber: Fiber, stateQueue: StateQueue, priorityLevel : PriorityLevel): void { const { scheduleLowPriWork } = getScheduler(); fiber.stateQueue = stateQueue; + // Schedule update on the alternate as well, since we don't know which tree + // is current. + // $FlowFixMe: Intersection issue. Don't know why it's only happening here. + const { alternate } = fiber; + if (alternate !== null) { + alternate.stateQueue = stateQueue; + } while (true) { if (fiber.pendingWorkPriority === NoWork || fiber.pendingWorkPriority >= priorityLevel) { fiber.pendingWorkPriority = priorityLevel; } + if (alternate !== null) { + if (alternate.pendingWorkPriority === NoWork || + alternate.pendingWorkPriority >= priorityLevel) { + alternate.pendingWorkPriority = priorityLevel; + } + } // Duck type root if (fiber.stateNode && fiber.stateNode.containerInfo) { const root : FiberRoot = (fiber.stateNode : any); @@ -139,21 +152,8 @@ module.exports = function(config : HostConfig, getSchedu const updater = { enqueueSetState(instance, partialState) { const fiber = instance._fiber; - let stateQueue = fiber.stateQueue; - - // Append to pending state queue - if (stateQueue === null) { - stateQueue = createStateQueue(partialState); - } else { - addToQueue(stateQueue, partialState); - } - - // Must schedule an update on both alternates, because we don't know tree - // is current. + const stateQueue = addToQueue(fiber.stateQueue, partialState); scheduleUpdate(fiber, stateQueue, LowPriority); - if (fiber.alternate) { - scheduleUpdate(fiber.alternate, stateQueue, LowPriority); - } }, }; diff --git a/src/renderers/shared/fiber/ReactFiberStateQueue.js b/src/renderers/shared/fiber/ReactFiberStateQueue.js index e8d6c7385e2..9ba30633611 100644 --- a/src/renderers/shared/fiber/ReactFiberStateQueue.js +++ b/src/renderers/shared/fiber/ReactFiberStateQueue.js @@ -26,15 +26,19 @@ exports.createStateQueue = function(partialState : mixed) : StateQueue { }; }; -exports.addToQueue = function(queue : StateQueue, partialState : mixed) { +exports.addToQueue = function(queue : StateQueue, partialState : mixed): StateQueue { const node = exports.createStateQueue(partialState); + if (queue === null) { + return node; + } if (queue.tail === null) { queue.next = node; } else { queue.tail.next = node; } queue.tail = node; -} + return queue; +}; exports.mergeStateQueue = function(prevState : any, props : any, queue : StateQueue) : any { if (queue === null) { From 691e053650a161afda9c2ad1dd02f21ec264e2f0 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 25 Jul 2016 09:36:08 -0700 Subject: [PATCH 05/15] Fix stateQueue typing --- src/renderers/shared/fiber/ReactFiber.js | 2 +- src/renderers/shared/fiber/ReactFiberBeginWork.js | 8 ++++++-- src/renderers/shared/fiber/ReactFiberStateQueue.js | 13 +++++-------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index 33128d85dd5..d6dc2bf3375 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -78,7 +78,7 @@ export type Fiber = Instance & { // 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. - stateQueue: StateQueue, + stateQueue: ?StateQueue, // The state used to create the output. This is a full state object. memoizedState: any, // Output is the return value of this fiber, or a linked list of return values diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 3004ce13d23..a8e97032d20 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -152,7 +152,9 @@ module.exports = function(config : HostConfig, getSchedu const updater = { enqueueSetState(instance, partialState) { const fiber = instance._fiber; - const stateQueue = addToQueue(fiber.stateQueue, partialState); + const stateQueue = fiber.stateQueue ? + addToQueue(fiber.stateQueue, partialState) : + createStateQueue(partialState); scheduleUpdate(fiber, stateQueue, LowPriority); }, }; @@ -181,7 +183,9 @@ module.exports = function(config : HostConfig, getSchedu state = instance.state || null; // The initial state must be added to the pending state queue in case // setState is called before the initial render. - workInProgress.stateQueue = createStateQueue(state); + if (state !== null) { + workInProgress.stateQueue = createStateQueue(state); + } // The instance needs access to the fiber so that it can schedule updates instance._fiber = workInProgress; instance.updater = updater; diff --git a/src/renderers/shared/fiber/ReactFiberStateQueue.js b/src/renderers/shared/fiber/ReactFiberStateQueue.js index 9ba30633611..59320028107 100644 --- a/src/renderers/shared/fiber/ReactFiberStateQueue.js +++ b/src/renderers/shared/fiber/ReactFiberStateQueue.js @@ -14,9 +14,9 @@ export type StateQueue = { partialState: any, - next: StateQueue, - tail: StateQueue -} | null; + next: StateQueue | null, + tail: StateQueue | null +}; exports.createStateQueue = function(partialState : mixed) : StateQueue { return { @@ -28,9 +28,6 @@ exports.createStateQueue = function(partialState : mixed) : StateQueue { exports.addToQueue = function(queue : StateQueue, partialState : mixed): StateQueue { const node = exports.createStateQueue(partialState); - if (queue === null) { - return node; - } if (queue.tail === null) { queue.next = node; } else { @@ -40,8 +37,8 @@ exports.addToQueue = function(queue : StateQueue, partialState : mixed): StateQu return queue; }; -exports.mergeStateQueue = function(prevState : any, props : any, queue : StateQueue) : any { - if (queue === null) { +exports.mergeStateQueue = function(prevState : any, props : any, queue : ?StateQueue) : any { + if (!queue) { return prevState; } let state = Object.assign({}, prevState); From d218158d229ee505a2901184fb2602e08c5645c2 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 26 Jul 2016 10:28:45 -0700 Subject: [PATCH 06/15] Clean up Use a union type for the head of StateQueue. --- .../shared/fiber/ReactFiberBeginWork.js | 23 +++++-------- .../shared/fiber/ReactFiberStateQueue.js | 34 ++++++++++++------- .../fiber/__tests__/ReactIncremental-test.js | 2 +- 3 files changed, 32 insertions(+), 27 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index a8e97032d20..c1ad706b5b1 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -119,20 +119,18 @@ module.exports = function(config : HostConfig, getSchedu fiber.stateQueue = stateQueue; // Schedule update on the alternate as well, since we don't know which tree // is current. - // $FlowFixMe: Intersection issue. Don't know why it's only happening here. - const { alternate } = fiber; - if (alternate !== null) { - alternate.stateQueue = stateQueue; + if (fiber.alternate !== null) { + fiber.alternate.stateQueue = stateQueue; } while (true) { if (fiber.pendingWorkPriority === NoWork || fiber.pendingWorkPriority >= priorityLevel) { fiber.pendingWorkPriority = priorityLevel; } - if (alternate !== null) { - if (alternate.pendingWorkPriority === NoWork || - alternate.pendingWorkPriority >= priorityLevel) { - alternate.pendingWorkPriority = priorityLevel; + if (fiber.alternate !== null) { + if (fiber.alternate.pendingWorkPriority === NoWork || + fiber.alternate.pendingWorkPriority >= priorityLevel) { + fiber.alternate.pendingWorkPriority = priorityLevel; } } // Duck type root @@ -169,12 +167,9 @@ module.exports = function(config : HostConfig, getSchedu } // Compute the state using the memoized state and the pending state queue. var stateQueue = workInProgress.stateQueue; - var state; - if (!current) { - state = mergeStateQueue(null, props, stateQueue); - } else { - state = mergeStateQueue(current.memoizedState, props, stateQueue); - } + var state = current ? + mergeStateQueue(stateQueue, current.memoizedState, props) : + mergeStateQueue(stateQueue, null, props); var instance = workInProgress.stateNode; if (!instance) { diff --git a/src/renderers/shared/fiber/ReactFiberStateQueue.js b/src/renderers/shared/fiber/ReactFiberStateQueue.js index 59320028107..beceeb07dd2 100644 --- a/src/renderers/shared/fiber/ReactFiberStateQueue.js +++ b/src/renderers/shared/fiber/ReactFiberStateQueue.js @@ -12,23 +12,32 @@ 'use strict'; -export type StateQueue = { +type StateQueueNode = { partialState: any, - next: StateQueue | null, - tail: StateQueue | null + callback: ?Function, + next: ?StateQueueNode, +}; + +export type StateQueue = StateQueueNode & { + tail: ?StateQueueNode }; exports.createStateQueue = function(partialState : mixed) : StateQueue { return { partialState, + callback: null, next: null, tail: null, }; }; -exports.addToQueue = function(queue : StateQueue, partialState : mixed): StateQueue { - const node = exports.createStateQueue(partialState); - if (queue.tail === null) { +exports.addToQueue = function(queue : StateQueue, partialState : mixed) : StateQueue { + const node = { + partialState, + callback: null, + next: null, + }; + if (!queue.tail) { queue.next = node; } else { queue.tail.next = node; @@ -37,16 +46,17 @@ exports.addToQueue = function(queue : StateQueue, partialState : mixed): StateQu return queue; }; -exports.mergeStateQueue = function(prevState : any, props : any, queue : ?StateQueue) : any { - if (!queue) { +exports.mergeStateQueue = function(queue : ?StateQueue, prevState : any, props : any) : any { + let node : ?StateQueueNode = queue; + if (!node) { return prevState; } let state = Object.assign({}, prevState); do { - const partialState = typeof queue.partialState === 'function' ? - queue.partialState(state, props) : - queue.partialState; + const partialState = typeof node.partialState === 'function' ? + node.partialState(state, props) : + node.partialState; state = Object.assign(state, partialState); - } while (queue = queue.next); + } while (node = node.next); return state; }; diff --git a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js index e7dccc7603c..5179acec515 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js @@ -648,8 +648,8 @@ describe('ReactIncremental', () => { instance.setState(updater); ReactNoop.flush(); expect(instance.state.num).toEqual(2); - ReactNoop.render(); instance.setState(updater); + ReactNoop.render(); ReactNoop.flush(); expect(instance.state.num).toEqual(6); }); From 97dac74c402e08f0edee7045e9d0406bdafcfc9c Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 26 Jul 2016 13:23:11 -0700 Subject: [PATCH 07/15] Use ReactInstanceMap Move ReactInstanceMap to src/renderers/shared/shared to indicate that this logic is shared across implementations. --- src/renderers/shared/fiber/ReactFiberBeginWork.js | 5 +++-- .../shared/{stack/reconciler => shared}/ReactInstanceMap.js | 0 2 files changed, 3 insertions(+), 2 deletions(-) rename src/renderers/shared/{stack/reconciler => shared}/ReactInstanceMap.js (100%) diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index c1ad706b5b1..6cda6b7df88 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -46,6 +46,7 @@ var { addToQueue, mergeStateQueue, } = require('ReactFiberStateQueue'); +var ReactInstanceMap = require('ReactInstanceMap'); module.exports = function(config : HostConfig, getScheduler : () => Scheduler) { @@ -149,7 +150,7 @@ module.exports = function(config : HostConfig, getSchedu // Class component state updater const updater = { enqueueSetState(instance, partialState) { - const fiber = instance._fiber; + const fiber = ReactInstanceMap.get(instance); const stateQueue = fiber.stateQueue ? addToQueue(fiber.stateQueue, partialState) : createStateQueue(partialState); @@ -182,7 +183,7 @@ module.exports = function(config : HostConfig, getSchedu workInProgress.stateQueue = createStateQueue(state); } // The instance needs access to the fiber so that it can schedule updates - instance._fiber = workInProgress; + ReactInstanceMap.set(instance, workInProgress); instance.updater = updater; } else if (typeof instance.shouldComponentUpdate === 'function') { if (workInProgress.memoizedProps !== null) { diff --git a/src/renderers/shared/stack/reconciler/ReactInstanceMap.js b/src/renderers/shared/shared/ReactInstanceMap.js similarity index 100% rename from src/renderers/shared/stack/reconciler/ReactInstanceMap.js rename to src/renderers/shared/shared/ReactInstanceMap.js From f7dab22ac9a3fe6d148ea8fbd6447e7122881885 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 27 Jul 2016 13:07:08 -0700 Subject: [PATCH 08/15] Ensure that setState update function's context is undefined --- src/renderers/shared/fiber/ReactFiberStateQueue.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberStateQueue.js b/src/renderers/shared/fiber/ReactFiberStateQueue.js index beceeb07dd2..d410f8d0d69 100644 --- a/src/renderers/shared/fiber/ReactFiberStateQueue.js +++ b/src/renderers/shared/fiber/ReactFiberStateQueue.js @@ -53,9 +53,13 @@ exports.mergeStateQueue = function(queue : ?StateQueue, prevState : any, props : } let state = Object.assign({}, prevState); do { - const partialState = typeof node.partialState === 'function' ? - node.partialState(state, props) : - node.partialState; + let partialState; + if (typeof node.partialState === 'function') { + const updateFn = node.partialState; + partialState = updateFn(state, props); + } else { + partialState = node.partialState; + } state = Object.assign(state, partialState); } while (node = node.next); return state; From c6db7f73bebd135dcb2dce2eaebed64c54627727 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 4 Aug 2016 14:27:55 -0700 Subject: [PATCH 09/15] Rename stateQueue -> updateQueue Also cleans up some types. --- src/renderers/shared/fiber/ReactFiber.js | 10 ++-- .../shared/fiber/ReactFiberBeginWork.js | 40 ++++++++-------- .../shared/fiber/ReactFiberScheduler.js | 4 +- ...StateQueue.js => ReactFiberUpdateQueue.js} | 47 +++++++++++-------- 4 files changed, 54 insertions(+), 47 deletions(-) rename src/renderers/shared/fiber/{ReactFiberStateQueue.js => ReactFiberUpdateQueue.js} (51%) diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index d6dc2bf3375..495c615c95e 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -15,7 +15,7 @@ import type { ReactCoroutine, ReactYield } from 'ReactCoroutine'; import type { TypeOfWork } from 'ReactTypeOfWork'; import type { PriorityLevel } from 'ReactPriorityLevel'; -import type { StateQueue } from 'ReactFiberStateQueue'; +import type { UpdateQueue } from 'ReactFiberUpdateQueue'; var ReactTypeOfWork = require('ReactTypeOfWork'); var { @@ -78,7 +78,7 @@ export type Fiber = Instance & { // 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. - stateQueue: ?StateQueue, + updateQueue: ?UpdateQueue, // The state used to create the output. This is a full state object. memoizedState: any, // Output is the return value of this fiber, or a linked list of return values @@ -156,7 +156,7 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber { pendingProps: null, memoizedProps: null, - stateQueue: null, + updateQueue: null, memoizedState: null, output: null, @@ -199,7 +199,7 @@ 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.stateQueue = fiber.stateQueue; + alt.updateQueue = fiber.updateQueue; alt.pendingWorkPriority = priorityLevel; alt.child = fiber.child; @@ -225,7 +225,7 @@ 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.stateQueue = fiber.stateQueue; + alt.updateQueue = fiber.updateQueue; alt.pendingWorkPriority = priorityLevel; alt.memoizedProps = fiber.memoizedProps; diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 6cda6b7df88..36ee2b9d781 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -18,7 +18,7 @@ import type { FiberRoot } from 'ReactFiberRoot'; import type { HostConfig } from 'ReactFiberReconciler'; import type { Scheduler } from 'ReactFiberScheduler'; import type { PriorityLevel } from 'ReactPriorityLevel'; -import type { StateQueue } from 'ReactFiberStateQueue'; +import type { UpdateQueue } from 'ReactFiberUpdateQueue'; var { reconcileChildFibers, @@ -42,10 +42,10 @@ var { OffscreenPriority, } = require('ReactPriorityLevel'); var { - createStateQueue, + createUpdateQueue, addToQueue, - mergeStateQueue, -} = require('ReactFiberStateQueue'); + mergeUpdateQueue, +} = require('ReactFiberUpdateQueue'); var ReactInstanceMap = require('ReactInstanceMap'); module.exports = function(config : HostConfig, getScheduler : () => Scheduler) { @@ -115,13 +115,13 @@ module.exports = function(config : HostConfig, getSchedu return workInProgress.child; } - function scheduleUpdate(fiber: Fiber, stateQueue: StateQueue, priorityLevel : PriorityLevel): void { + function scheduleUpdate(fiber: Fiber, updateQueue: UpdateQueue, priorityLevel : PriorityLevel): void { const { scheduleLowPriWork } = getScheduler(); - fiber.stateQueue = stateQueue; + fiber.updateQueue = updateQueue; // Schedule update on the alternate as well, since we don't know which tree // is current. if (fiber.alternate !== null) { - fiber.alternate.stateQueue = stateQueue; + fiber.alternate.updateQueue = updateQueue; } while (true) { if (fiber.pendingWorkPriority === NoWork || @@ -151,10 +151,10 @@ module.exports = function(config : HostConfig, getSchedu const updater = { enqueueSetState(instance, partialState) { const fiber = ReactInstanceMap.get(instance); - const stateQueue = fiber.stateQueue ? - addToQueue(fiber.stateQueue, partialState) : - createStateQueue(partialState); - scheduleUpdate(fiber, stateQueue, LowPriority); + const updateQueue = fiber.updateQueue ? + addToQueue(fiber.updateQueue, partialState) : + createUpdateQueue(partialState); + scheduleUpdate(fiber, updateQueue, LowPriority); }, }; @@ -166,21 +166,22 @@ module.exports = function(config : HostConfig, getSchedu if (!props && current) { props = current.memoizedProps; } - // Compute the state using the memoized state and the pending state queue. - var stateQueue = workInProgress.stateQueue; - var state = current ? - mergeStateQueue(stateQueue, current.memoizedState, props) : - mergeStateQueue(stateQueue, null, props); + // 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); state = instance.state || null; - // The initial state must be added to the pending state queue in case + // The initial state must be added to the update queue in case // setState is called before the initial render. if (state !== null) { - workInProgress.stateQueue = createStateQueue(state); + workInProgress.updateQueue = createUpdateQueue(state); } // The instance needs access to the fiber so that it can schedule updates ReactInstanceMap.set(instance, workInProgress); @@ -331,8 +332,7 @@ module.exports = function(config : HostConfig, getSchedu workInProgress.memoizedProps !== null && workInProgress.pendingProps === workInProgress.memoizedProps )) && - workInProgress.stateQueue === null - ) { + workInProgress.updateQueue === null) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 2136907d1af..122a64fcd5e 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -143,9 +143,9 @@ module.exports = function(config : HostConfig) { // 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.stateQueue = null; + workInProgress.updateQueue = null; if (current) { - current.stateQueue = null; + current.updateQueue = null; } const returnFiber = workInProgress.return; diff --git a/src/renderers/shared/fiber/ReactFiberStateQueue.js b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js similarity index 51% rename from src/renderers/shared/fiber/ReactFiberStateQueue.js rename to src/renderers/shared/fiber/ReactFiberUpdateQueue.js index d410f8d0d69..57329b25229 100644 --- a/src/renderers/shared/fiber/ReactFiberStateQueue.js +++ b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js @@ -6,53 +6,59 @@ * 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 ReactFiberStateQueue + * @providesModule ReactFiberUpdateQueue * @flow */ 'use strict'; -type StateQueueNode = { +type UpdateQueueNode = { partialState: any, callback: ?Function, - next: ?StateQueueNode, + next: ?UpdateQueueNode, }; -export type StateQueue = StateQueueNode & { - tail: ?StateQueueNode +export type UpdateQueue = UpdateQueueNode & { + tail: UpdateQueueNode }; -exports.createStateQueue = function(partialState : mixed) : StateQueue { - return { +exports.createUpdateQueue = function(partialState : mixed) : UpdateQueue { + const queue = { partialState, callback: null, next: null, - tail: null, + tail: (null : any), }; + queue.tail = queue; + return queue; }; -exports.addToQueue = function(queue : StateQueue, partialState : mixed) : StateQueue { +exports.addToQueue = function(queue : UpdateQueue, partialState : mixed) : UpdateQueue { const node = { partialState, callback: null, next: null, }; - if (!queue.tail) { - queue.next = node; - } else { - queue.tail.next = node; - } + queue.tail.next = node; queue.tail = node; return queue; }; -exports.mergeStateQueue = function(queue : ?StateQueue, prevState : any, props : any) : any { - let node : ?StateQueueNode = queue; - if (!node) { - return prevState; +exports.callCallbacks = function(queue : UpdateQueue, partialState : mixed) { + let node : ?UpdateQueueNode = queue; + while (node) { + if (node.callback) { + const { callback } = node; + callback(); + } + node = node.next; } +}; + +exports.mergeUpdateQueue = function(queue : UpdateQueue, prevState : any, props : any) : any { + let node : ?UpdateQueueNode = queue; let state = Object.assign({}, prevState); - do { + while (node) { let partialState; if (typeof node.partialState === 'function') { const updateFn = node.partialState; @@ -61,6 +67,7 @@ exports.mergeStateQueue = function(queue : ?StateQueue, prevState : any, props : partialState = node.partialState; } state = Object.assign(state, partialState); - } while (node = node.next); + node = node.next; + } return state; }; From 1d49237299fdbf8006c0eab8ebb6ba1386434036 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 4 Aug 2016 17:56:48 -0700 Subject: [PATCH 10/15] Update callbacks Callbacks are stored on the same queue as updates. They care called during the commit phase, after the updates have been flushed. Because the update queue is cleared during the completion phase (before commit), a new field has been added to fiber called callbackList. The queue is transferred from updateQueue to callbackList during completion. During commit, the list is reset. Need a test to confirm that callbacks are not lost if an update is preempted. --- src/renderers/shared/fiber/ReactFiber.js | 5 ++++ .../shared/fiber/ReactFiberBeginWork.js | 12 ++++++++ .../shared/fiber/ReactFiberCommitWork.js | 9 ++++++ .../shared/fiber/ReactFiberCompleteWork.js | 11 +++++-- .../shared/fiber/ReactFiberUpdateQueue.js | 20 ++++++++++--- .../ReactIncrementalSideEffects-test.js | 30 +++++++++++++++++++ 6 files changed, 80 insertions(+), 7 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index 495c615c95e..7dadfbe47a9 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -81,6 +81,8 @@ export type Fiber = Instance & { 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. @@ -158,6 +160,7 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber { memoizedProps: null, updateQueue: null, memoizedState: null, + callbackList: null, output: null, nextEffect: null, @@ -200,6 +203,7 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi 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; @@ -226,6 +230,7 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi // 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; diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 36ee2b9d781..c49f16ce918 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -44,6 +44,7 @@ var { var { createUpdateQueue, addToQueue, + addCallbackToQueue, mergeUpdateQueue, } = require('ReactFiberUpdateQueue'); var ReactInstanceMap = require('ReactInstanceMap'); @@ -156,6 +157,17 @@ module.exports = function(config : HostConfig, getSchedu createUpdateQueue(partialState); 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) { diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index 2e3e8ae3d02..d8590033833 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -22,6 +22,7 @@ var { HostContainer, HostComponent, } = ReactTypeOfWork; +var { callCallbacks } = require('ReactFiberUpdateQueue'); module.exports = function(config : HostConfig) { @@ -31,6 +32,14 @@ module.exports = function(config : HostConfig) { function commitWork(current : ?Fiber, finishedWork : Fiber) : void { switch (finishedWork.tag) { case ClassComponent: { + if (finishedWork.callbackList) { + const { callbackList } = finishedWork; + finishedWork.callbackList = null; + if (finishedWork.alternate) { + finishedWork.alternate.callbackList = null; + } + callCallbacks(callbackList, finishedWork.stateNode); + } // TODO: Fire componentDidMount/componentDidUpdate, update refs return; } diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index fdeb1f5e56c..6d6f9598c41 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -46,7 +46,6 @@ module.exports = function(config : HostConfig) { } } - /* // 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(config : HostConfig) { } workInProgress.lastEffect = workInProgress; } - */ function transferOutput(child : ?Fiber, returnFiber : Fiber) { // If we have a single result, we just pass that through as the output to @@ -133,9 +131,16 @@ module.exports = function(config : HostConfig) { 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. Copy it from there. + // merged it and assigned it to the instance. Transfer it from there. const state = workInProgress.stateNode.state; workInProgress.memoizedState = state; + // Transfer update queue to callbackList field so callbacks can be + // called during commit phase. + workInProgress.callbackList = workInProgress.updateQueue; + if (current) { + current.callbackList = workInProgress.callbackList; + } + markForPostEffect(workInProgress); return null; case HostContainer: transferOutput(workInProgress.child, workInProgress); diff --git a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js index 57329b25229..159b0ceb095 100644 --- a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js +++ b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js @@ -15,6 +15,7 @@ type UpdateQueueNode = { partialState: any, callback: ?Function, + callbackWasCalled: boolean, next: ?UpdateQueueNode, }; @@ -26,6 +27,7 @@ exports.createUpdateQueue = function(partialState : mixed) : UpdateQueue { const queue = { partialState, callback: null, + callbackWasCalled: false, next: null, tail: (null : any), }; @@ -37,6 +39,7 @@ exports.addToQueue = function(queue : UpdateQueue, partialState : mixed) : Updat const node = { partialState, callback: null, + callbackWasCalled: false, next: null, }; queue.tail.next = node; @@ -44,12 +47,21 @@ exports.addToQueue = function(queue : UpdateQueue, partialState : mixed) : Updat return queue; }; -exports.callCallbacks = function(queue : UpdateQueue, partialState : mixed) { +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) { - const { callback } = node; - callback(); + if (node.callback && !node.callbackWasCalled) { + node.callbackWasCalled = true; + node.callback.call(context); } node = node.next; } diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js index 63983720df8..f49f09592ee 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js @@ -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 ; + } + } + + ReactNoop.render(); + 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. }); From d8c24cfa78bd00691bf20dc1ad2750de84dd8ad4 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 4 Aug 2016 18:55:57 -0700 Subject: [PATCH 11/15] replaceState Adds a field to UpdateQueue that indicates whether an update should replace the previous state completely. --- .../shared/fiber/ReactFiberBeginWork.js | 6 ++++ .../shared/fiber/ReactFiberUpdateQueue.js | 6 ++-- .../fiber/__tests__/ReactIncremental-test.js | 29 +++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index c49f16ce918..2f80e30a529 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -157,6 +157,12 @@ module.exports = function(config : HostConfig, getSchedu 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); + }, enqueueCallback(instance, callback) { const fiber = ReactInstanceMap.get(instance); let updateQueue = fiber.updateQueue ? diff --git a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js index 159b0ceb095..97039994b17 100644 --- a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js +++ b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js @@ -20,6 +20,7 @@ type UpdateQueueNode = { }; export type UpdateQueue = UpdateQueueNode & { + isReplace: boolean, tail: UpdateQueueNode }; @@ -29,6 +30,7 @@ exports.createUpdateQueue = function(partialState : mixed) : UpdateQueue { callback: null, callbackWasCalled: false, next: null, + isReplace: false, tail: (null : any), }; queue.tail = queue; @@ -69,7 +71,7 @@ exports.callCallbacks = function(queue : UpdateQueue, context : any) { exports.mergeUpdateQueue = function(queue : UpdateQueue, prevState : any, props : any) : any { let node : ?UpdateQueueNode = queue; - let state = Object.assign({}, prevState); + let state = queue.isReplace ? null : Object.assign({}, prevState); while (node) { let partialState; if (typeof node.partialState === 'function') { @@ -78,7 +80,7 @@ exports.mergeUpdateQueue = function(queue : UpdateQueue, prevState : any, props } else { partialState = node.partialState; } - state = Object.assign(state, partialState); + state = Object.assign(state || {}, partialState); node = node.next; } return state; diff --git a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js index 5179acec515..f3caa3d0a8f 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js @@ -653,4 +653,33 @@ describe('ReactIncremental', () => { ReactNoop.flush(); expect(instance.state.num).toEqual(6); }); + + it('can replaceState', () => { + let instance; + const Bar = React.createClass({ + getInitialState() { + instance = this; + return { a: 'a' }; + }, + render() { + return
{this.props.children}
; + }, + }); + + function Foo() { + return ( +
+ +
+ ); + } + + ReactNoop.render(); + ReactNoop.flush(); + instance.setState({ b: 'b' }); + instance.setState({ c: 'c' }); + instance.replaceState({ d: 'd' }); + ReactNoop.flush(); + expect(instance.state).toEqual({ d: 'd' }); + }); }); From f514662ca0959fd42031396c5c21dc3241abbf66 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 4 Aug 2016 19:16:29 -0700 Subject: [PATCH 12/15] forceUpdate Adds a field to the update queue that causes shouldComponentUpdate to be skipped. --- .../shared/fiber/ReactFiberBeginWork.js | 9 ++++- .../shared/fiber/ReactFiberUpdateQueue.js | 2 + .../fiber/__tests__/ReactIncremental-test.js | 40 +++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 2f80e30a529..cab153c1122 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -163,6 +163,12 @@ module.exports = function(config : HostConfig, getSchedu 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 ? @@ -204,7 +210,8 @@ module.exports = function(config : HostConfig, getSchedu // 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') { + } 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 diff --git a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js index 97039994b17..aab252b6558 100644 --- a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js +++ b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js @@ -21,6 +21,7 @@ type UpdateQueueNode = { export type UpdateQueue = UpdateQueueNode & { isReplace: boolean, + isForced: boolean, tail: UpdateQueueNode }; @@ -31,6 +32,7 @@ exports.createUpdateQueue = function(partialState : mixed) : UpdateQueue { callbackWasCalled: false, next: null, isReplace: false, + isForced: false, tail: (null : any), }; queue.tail = queue; diff --git a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js index f3caa3d0a8f..5213acfa48e 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js @@ -682,4 +682,44 @@ describe('ReactIncremental', () => { ReactNoop.flush(); expect(instance.state).toEqual({ d: 'd' }); }); + + it('can forceUpdate', () => { + const ops = []; + + function Baz() { + ops.push('Baz'); + return
; + } + + let instance; + class Bar extends React.Component { + constructor() { + super(); + instance = this; + } + shouldComponentUpdate() { + return false; + } + render() { + ops.push('Bar'); + return ; + } + } + + function Foo() { + ops.push('Foo'); + return ( +
+ +
+ ); + } + + ReactNoop.render(); + ReactNoop.flush(); + expect(ops).toEqual(['Foo', 'Bar', 'Baz']); + instance.forceUpdate(); + ReactNoop.flush(); + expect(ops).toEqual(['Foo', 'Bar', 'Baz', 'Bar', 'Baz']); + }); }); From 0ca1cea26ae66fa26973c4f3912da7e7b8ed47f5 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Sat, 6 Aug 2016 16:47:48 -0700 Subject: [PATCH 13/15] Don't mutate current tree before work is committed. We should be able to abort an update without any side-effects to the current tree. This fixes a few cases where that was broken. The callback list should only ever be set on the workInProgress. There's no reason to add it to the current tree because they're not needed after they are called during the commit phase. Also found a bug where the memoizedProps were set to null in the case of an update, because the pendingProps were null. Fixed by transfering the props from the instance, like we were already doing with state. Added a test to ensure that setState can be called inside a callback. --- .../shared/fiber/ReactFiberCommitWork.js | 10 +++-- .../shared/fiber/ReactFiberCompleteWork.js | 8 ++-- .../shared/fiber/ReactFiberScheduler.js | 3 -- .../fiber/__tests__/ReactIncremental-test.js | 38 +++++++++++++++++++ 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index d8590033833..beb773a2e77 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -32,12 +32,16 @@ module.exports = function(config : HostConfig) { 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; - if (finishedWork.alternate) { - finishedWork.alternate.callbackList = null; - } callCallbacks(callbackList, finishedWork.stateNode); } // TODO: Fire componentDidMount/componentDidUpdate, update refs diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index 6d6f9598c41..d4687f45745 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -132,14 +132,14 @@ module.exports = function(config : HostConfig) { 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. - const state = workInProgress.stateNode.state; + // 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; - if (current) { - current.callbackList = workInProgress.callbackList; - } markForPostEffect(workInProgress); return null; case HostContainer: diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 122a64fcd5e..874c24928cc 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -144,9 +144,6 @@ module.exports = function(config : HostConfig) { // to the system not to redo any work here. workInProgress.pendingProps = null; workInProgress.updateQueue = null; - if (current) { - current.updateQueue = null; - } const returnFiber = workInProgress.return; diff --git a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js index 5213acfa48e..c543de45499 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js @@ -654,6 +654,44 @@ describe('ReactIncremental', () => { 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
{this.props.children}
; + } + } + + function Foo({ multiplier }) { + return ( +
+ +
+ ); + } + + function updater(state, props) { + return { num: state.num * props.multiplier }; + } + + function callback() { + this.setState({ called: true }); + } + + ReactNoop.render(); + 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({ From f3a2dc252edadd360b28b653cde082e066e478be Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Fri, 2 Sep 2016 17:23:09 -0700 Subject: [PATCH 14/15] Check for truthiness of alternate This is unfortunate since we agreed on using the `null | Fiber` convention instead of `?Fiber` but haven't upgraded yet and this is the pattern I've been using everywhere else so far. --- src/renderers/shared/fiber/ReactFiberBeginWork.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index cab153c1122..80b22b0dd55 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -121,7 +121,7 @@ module.exports = function(config : HostConfig, getSchedu fiber.updateQueue = updateQueue; // Schedule update on the alternate as well, since we don't know which tree // is current. - if (fiber.alternate !== null) { + if (fiber.alternate) { fiber.alternate.updateQueue = updateQueue; } while (true) { @@ -129,7 +129,7 @@ module.exports = function(config : HostConfig, getSchedu fiber.pendingWorkPriority >= priorityLevel) { fiber.pendingWorkPriority = priorityLevel; } - if (fiber.alternate !== null) { + if (fiber.alternate) { if (fiber.alternate.pendingWorkPriority === NoWork || fiber.alternate.pendingWorkPriority >= priorityLevel) { fiber.alternate.pendingWorkPriority = priorityLevel; From 27d1210c1d00e9b34839fd5596a74d8af52ffa7f Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 5 Sep 2016 23:05:53 -0700 Subject: [PATCH 15/15] Log the updateQueue in dumpTree This also buffers all rows into a single console.log call. This is because jest nows adds the line number of each console.log call which becomes quite noisy for these trees. --- src/renderers/noop/ReactNoop.js | 48 ++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/src/renderers/noop/ReactNoop.js b/src/renderers/noop/ReactNoop.js index 390c4f6ac38..4d1bfb46b83 100644 --- a/src/renderers/noop/ReactNoop.js +++ b/src/renderers/noop/ReactNoop.js @@ -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, 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); }, };