diff --git a/package.json b/package.json index e042615c0232b..077bac9d161f1 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "glob-stream": "^6.1.0", "gzip-js": "~0.3.2", "gzip-size": "^3.0.0", + "jasmine-check": "^1.0.0-rc.0", "jest": "20.1.0-delta.1", "jest-config": "20.1.0-delta.1", "jest-jasmine2": "20.1.0-delta.1", diff --git a/scripts/jest/test-framework-setup.js b/scripts/jest/test-framework-setup.js index ca4706516710c..28ec0defae887 100644 --- a/scripts/jest/test-framework-setup.js +++ b/scripts/jest/test-framework-setup.js @@ -68,4 +68,6 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) { return expectation; }; global.expectDev = expectDev; + + require('jasmine-check').install(); } diff --git a/src/renderers/dom/fiber/__tests__/ReactDOMFiberAsync-test.js b/src/renderers/dom/fiber/__tests__/ReactDOMFiberAsync-test.js index b432af4701a23..e8b4726e5c2f5 100644 --- a/src/renderers/dom/fiber/__tests__/ReactDOMFiberAsync-test.js +++ b/src/renderers/dom/fiber/__tests__/ReactDOMFiberAsync-test.js @@ -284,8 +284,8 @@ describe('ReactDOMFiberAsync', () => { // Flush the async updates jest.runAllTimers(); - expect(container.textContent).toEqual('BCAD'); - expect(ops).toEqual(['BC', 'BCAD']); + expect(container.textContent).toEqual('ABCD'); + expect(ops).toEqual(['BC', 'ABCD']); }); }); }); diff --git a/src/renderers/noop/ReactNoopEntry.js b/src/renderers/noop/ReactNoopEntry.js index bf964eb84e86b..a1b7cf099a022 100644 --- a/src/renderers/noop/ReactNoopEntry.js +++ b/src/renderers/noop/ReactNoopEntry.js @@ -405,7 +405,7 @@ var ReactNoop = { logHostInstances(container.children, depth + 1); } - function logUpdateQueue(updateQueue: UpdateQueue, depth) { + function logUpdateQueue(updateQueue: UpdateQueue, depth) { log(' '.repeat(depth + 1) + 'QUEUED UPDATES'); const firstUpdate = updateQueue.first; if (!firstUpdate) { diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index 3c81c35e31af3..633911db34134 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -109,7 +109,7 @@ export type Fiber = {| memoizedProps: any, // The props used to create the output. // A queue of state updates and callbacks. - updateQueue: UpdateQueue | null, + updateQueue: UpdateQueue | null, // The state used to create the output memoizedState: any, diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 980c3b07663d3..59499e01ca057 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -24,7 +24,7 @@ var { reconcileChildFibersInPlace, cloneChildFibers, } = require('ReactChildFiber'); -var {beginUpdateQueue} = require('ReactFiberUpdateQueue'); +var {processUpdateQueue} = require('ReactFiberUpdateQueue'); var ReactTypeOfWork = require('ReactTypeOfWork'); var { getMaskedContext, @@ -71,8 +71,8 @@ module.exports = function( config: HostConfig, hostContext: HostContext, hydrationContext: HydrationContext, - scheduleUpdate: (fiber: Fiber, expirationTime: ExpirationTime) => void, - getExpirationTime: (fiber: Fiber, forceAsync: boolean) => ExpirationTime, + scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void, + computeExpirationForFiber: (fiber: Fiber) => ExpirationTime, ) { const { shouldSetTextContent, @@ -95,8 +95,8 @@ module.exports = function( // resumeMountClassInstance, updateClassInstance, } = ReactFiberClassComponent( - scheduleUpdate, - getExpirationTime, + scheduleWork, + computeExpirationForFiber, memoizeProps, memoizeState, ); @@ -323,12 +323,11 @@ module.exports = function( const updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { const prevState = workInProgress.memoizedState; - const state = beginUpdateQueue( + const state = processUpdateQueue( current, workInProgress, updateQueue, null, - prevState, null, renderExpirationTime, ); @@ -720,7 +719,7 @@ module.exports = function( function memoizeState(workInProgress: Fiber, nextState: any) { workInProgress.memoizedState = nextState; // Don't reset the updateQueue, in case there are pending updates. Resetting - // is handled by beginUpdateQueue. + // is handled by processUpdateQueue. } function beginWork( diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js index 0e2182e0fc2b8..a9464b1af2dff 100644 --- a/src/renderers/shared/fiber/ReactFiberClassComponent.js +++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js @@ -25,10 +25,8 @@ var { isContextConsumer, } = require('ReactFiberContext'); var { - addUpdate, - addReplaceUpdate, - addForceUpdate, - beginUpdateQueue, + insertUpdateIntoFiber, + processUpdateQueue, } = require('ReactFiberUpdateQueue'); var {hasContextChanged} = require('ReactFiberContext'); var {isMounted} = require('ReactFiberTreeReflection'); @@ -77,8 +75,8 @@ if (__DEV__) { } module.exports = function( - scheduleUpdate: (fiber: Fiber, expirationTime: ExpirationTime) => void, - getExpirationTime: (fiber: Fiber, forceAsync: boolean) => ExpirationTime, + scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void, + computeExpirationForFiber: (fiber: Fiber) => ExpirationTime, memoizeProps: (workInProgress: Fiber, props: any) => void, memoizeState: (workInProgress: Fiber, state: any) => void, ) { @@ -87,33 +85,60 @@ module.exports = function( isMounted, enqueueSetState(instance, partialState, callback) { const fiber = ReactInstanceMap.get(instance); - const expirationTime = getExpirationTime(fiber, false); callback = callback === undefined ? null : callback; if (__DEV__) { warnOnInvalidCallback(callback, 'setState'); } - addUpdate(fiber, partialState, callback, expirationTime); - scheduleUpdate(fiber, expirationTime); + const expirationTime = computeExpirationForFiber(fiber); + const update = { + expirationTime, + partialState, + callback, + isReplace: false, + isForced: false, + nextCallback: null, + next: null, + }; + insertUpdateIntoFiber(fiber, update); + scheduleWork(fiber, expirationTime); }, enqueueReplaceState(instance, state, callback) { const fiber = ReactInstanceMap.get(instance); - const expirationTime = getExpirationTime(fiber, false); callback = callback === undefined ? null : callback; if (__DEV__) { warnOnInvalidCallback(callback, 'replaceState'); } - addReplaceUpdate(fiber, state, callback, expirationTime); - scheduleUpdate(fiber, expirationTime); + const expirationTime = computeExpirationForFiber(fiber); + const update = { + expirationTime, + partialState: state, + callback, + isReplace: true, + isForced: false, + nextCallback: null, + next: null, + }; + insertUpdateIntoFiber(fiber, update); + scheduleWork(fiber, expirationTime); }, enqueueForceUpdate(instance, callback) { const fiber = ReactInstanceMap.get(instance); - const expirationTime = getExpirationTime(fiber, false); callback = callback === undefined ? null : callback; if (__DEV__) { warnOnInvalidCallback(callback, 'forceUpdate'); } - addForceUpdate(fiber, callback, expirationTime); - scheduleUpdate(fiber, expirationTime); + const expirationTime = computeExpirationForFiber(fiber); + const update = { + expirationTime, + partialState: null, + callback, + isReplace: false, + isForced: true, + nextCallback: null, + next: null, + }; + insertUpdateIntoFiber(fiber, update); + scheduleWork(fiber, expirationTime); }, }; @@ -404,7 +429,7 @@ module.exports = function( const unmaskedContext = getUnmaskedContext(workInProgress); instance.props = props; - instance.state = state; + instance.state = workInProgress.memoizedState = state; instance.refs = emptyObject; instance.context = getMaskedContext(workInProgress, unmaskedContext); @@ -423,12 +448,11 @@ module.exports = function( // process them now. const updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { - instance.state = beginUpdateQueue( + instance.state = processUpdateQueue( current, workInProgress, updateQueue, instance, - state, props, renderExpirationTime, ); @@ -481,7 +505,7 @@ module.exports = function( // // Process the update queue before calling shouldComponentUpdate // const updateQueue = workInProgress.updateQueue; // if (updateQueue !== null) { - // newState = beginUpdateQueue( + // newState = processUpdateQueue( // workInProgress, // updateQueue, // instance, @@ -524,7 +548,7 @@ module.exports = function( // // componentWillMount may have called setState. Process the update queue. // const newUpdateQueue = workInProgress.updateQueue; // if (newUpdateQueue !== null) { - // newState = beginUpdateQueue( + // newState = processUpdateQueue( // workInProgress, // newUpdateQueue, // instance, @@ -590,12 +614,11 @@ module.exports = function( // TODO: Previous state can be null. let newState; if (workInProgress.updateQueue !== null) { - newState = beginUpdateQueue( + newState = processUpdateQueue( current, workInProgress, workInProgress.updateQueue, instance, - oldState, newProps, renderExpirationTime, ); diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index 32e7fd9bc7aa3..a6edb69939f34 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -30,12 +30,7 @@ var { clearCaughtError, } = require('ReactErrorUtils'); -var { - Placement, - Update, - Callback, - ContentReset, -} = require('ReactTypeOfSideEffect'); +var {Placement, Update, ContentReset} = require('ReactTypeOfSideEffect'); var invariant = require('fbjs/lib/invariant'); @@ -132,19 +127,19 @@ module.exports = function( } } } - if ( - finishedWork.effectTag & Callback && - finishedWork.updateQueue !== null - ) { - commitCallbacks(finishedWork, finishedWork.updateQueue, instance); + const updateQueue = finishedWork.updateQueue; + if (updateQueue !== null) { + commitCallbacks(updateQueue, instance); } return; } case HostRoot: { const updateQueue = finishedWork.updateQueue; if (updateQueue !== null) { - const instance = finishedWork.child && finishedWork.child.stateNode; - commitCallbacks(finishedWork, updateQueue, instance); + const instance = finishedWork.child !== null + ? finishedWork.child.stateNode + : null; + commitCallbacks(updateQueue, instance); } return; } diff --git a/src/renderers/shared/fiber/ReactFiberExpirationTime.js b/src/renderers/shared/fiber/ReactFiberExpirationTime.js index 202be36e37f15..8eeb26e781982 100644 --- a/src/renderers/shared/fiber/ReactFiberExpirationTime.js +++ b/src/renderers/shared/fiber/ReactFiberExpirationTime.js @@ -34,27 +34,20 @@ function msToExpirationTime(ms: number): ExpirationTime { exports.msToExpirationTime = msToExpirationTime; function ceiling(num: number, precision: number): number { - return (((((num * precision) | 0) + 1) / precision) | 0) + 1; + return (((num / precision) | 0) + 1) * precision; } -function bucket( +function computeExpirationBucket( currentTime: ExpirationTime, expirationInMs: number, - precisionInMs: number, + bucketSizeMs: number, ): ExpirationTime { return ceiling( currentTime + expirationInMs / UNIT_SIZE, - precisionInMs / UNIT_SIZE, + bucketSizeMs / UNIT_SIZE, ); } - -// Given the current clock time, returns an expiration time. We use rounding -// to batch like updates together. -function asyncExpirationTime(currentTime: ExpirationTime) { - // Should complete within ~1000ms. 1200ms max. - return bucket(currentTime, 1000, 200); -} -exports.asyncExpirationTime = asyncExpirationTime; +exports.computeExpirationBucket = computeExpirationBucket; // Given the current clock time and an expiration time, returns the // relative expiration time. Possible values include NoWork, Sync, Task, and diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 697f41ca3505d..e0972a0314a18 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -15,9 +15,6 @@ import type {FiberRoot} from 'ReactFiberRoot'; import type {ReactNodeList} from 'ReactTypes'; var ReactFeatureFlags = require('ReactFeatureFlags'); - -var {addTopLevelUpdate} = require('ReactFiberUpdateQueue'); - var { findCurrentUnmaskedContext, isContextProvider, @@ -27,6 +24,7 @@ var {createFiberRoot} = require('ReactFiberRoot'); var ReactFiberScheduler = require('ReactFiberScheduler'); var ReactInstanceMap = require('ReactInstanceMap'); var {HostComponent} = require('ReactTypeOfWork'); +var {insertUpdateIntoFiber} = require('ReactFiberUpdateQueue'); var emptyObject = require('fbjs/lib/emptyObject'); if (__DEV__) { @@ -265,8 +263,9 @@ module.exports = function( var {getPublicInstance} = config; var { - scheduleUpdate, - getExpirationTime, + computeAsyncExpiration, + computeExpirationForFiber, + scheduleWork, batchedUpdates, unbatchedUpdates, flushSync, @@ -296,17 +295,6 @@ module.exports = function( } } - // Check if the top-level element is an async wrapper component. If so, treat - // updates to the root as async. This is a bit weird but lets us avoid a separate - // `renderAsync` API. - const forceAsync = - ReactFeatureFlags.enableAsyncSubtreeAPI && - element != null && - element.type != null && - element.type.prototype != null && - (element.type.prototype: any).unstable_isAsyncReactComponent === true; - const expirationTime = getExpirationTime(current, forceAsync); - const nextState = {element}; callback = callback === undefined ? null : callback; if (__DEV__) { warning( @@ -316,8 +304,34 @@ module.exports = function( callback, ); } - addTopLevelUpdate(current, nextState, callback, expirationTime); - scheduleUpdate(current, expirationTime); + + let expirationTime; + // Check if the top-level element is an async wrapper component. If so, + // treat updates to the root as async. This is a bit weird but lets us + // avoid a separate `renderAsync` API. + if ( + ReactFeatureFlags.enableAsyncSubtreeAPI && + element != null && + element.type != null && + element.type.prototype != null && + (element.type.prototype: any).unstable_isAsyncReactComponent === true + ) { + expirationTime = computeAsyncExpiration(); + } else { + expirationTime = computeExpirationForFiber(current); + } + + const update = { + expirationTime, + partialState: {element}, + callback, + isReplace: false, + isForced: false, + nextCallback: null, + next: null, + }; + insertUpdateIntoFiber(current, update); + scheduleWork(current, expirationTime); } return { diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 13bcea3b5b432..50f16402d0662 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -59,7 +59,7 @@ var { Sync, Never, msToExpirationTime, - asyncExpirationTime, + computeExpirationBucket, relativeExpirationTime, } = require('ReactFiberExpirationTime'); @@ -165,8 +165,8 @@ module.exports = function( config, hostContext, hydrationContext, - scheduleUpdate, - getExpirationTime, + scheduleWork, + computeExpirationForFiber, ); const {completeWork} = ReactFiberCompleteWork( config, @@ -1373,11 +1373,58 @@ module.exports = function( } } - function scheduleUpdate(fiber: Fiber, expirationTime: ExpirationTime) { - return scheduleUpdateImpl(fiber, expirationTime, false); + function computeAsyncExpiration() { + // Given the current clock time, returns an expiration time. We use rounding + // to batch like updates together. + // Should complete within ~1000ms. 1200ms max. + const currentTime = recalculateCurrentTime(); + const expirationMs = 1000; + const bucketSizeMs = 200; + return computeExpirationBucket(currentTime, expirationMs, bucketSizeMs); } - function scheduleUpdateImpl( + function computeExpirationForFiber(fiber: Fiber) { + let expirationTime; + if (expirationContext !== NoWork) { + // An explicit expiration context was set; + expirationTime = expirationContext; + } else if (isPerformingWork) { + if (isCommitting) { + // Updates that occur during the commit phase should have sync priority + // by default. + expirationTime = Sync; + } else { + // Updates during the render phase should expire at the same time as + // the work that is being rendered. + expirationTime = nextRenderExpirationTime; + } + } else { + // No explicit expiration context was set, and we're not currently + // performing work. Calculate a new expiration time. + if (useSyncScheduling && !(fiber.internalContextTag & AsyncUpdates)) { + // This is a sync update + expirationTime = Sync; + } else { + // This is an async update + expirationTime = computeAsyncExpiration(); + } + } + + if ( + expirationTime === Sync && + (isBatchingUpdates || (isUnbatchingUpdates && isCommitting)) + ) { + // If we're in a batch, downgrade sync to task. + expirationTime = Task; + } + return expirationTime; + } + + function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) { + return scheduleWorkImpl(fiber, expirationTime, false); + } + + function scheduleWorkImpl( fiber: Fiber, expirationTime: ExpirationTime, isErrorRecovery: boolean, @@ -1483,53 +1530,8 @@ module.exports = function( } } - function getExpirationTime( - fiber: Fiber, - forceAsync: boolean, - ): ExpirationTime { - let expirationTime; - if (expirationContext !== NoWork) { - // An explicit expiration context was set; - expirationTime = expirationContext; - } else if (isPerformingWork) { - if (isCommitting) { - // Updates that occur during the commit phase should have task priority - // by default. - expirationTime = Sync; - } else { - // Updates during the render phase should expire at the same time as - // the work that is being rendered. - expirationTime = nextRenderExpirationTime; - } - } else { - // No explicit expiration context was set, and we're not currently - // performing work. Calculate a new expiration time. - if ( - useSyncScheduling && - !(fiber.internalContextTag & AsyncUpdates) && - !forceAsync - ) { - // This is a sync update - expirationTime = Sync; - } else { - // This is an async update - const currentTime = recalculateCurrentTime(); - expirationTime = asyncExpirationTime(currentTime); - } - } - - if ( - expirationTime === Sync && - (isBatchingUpdates || (isUnbatchingUpdates && isCommitting)) - ) { - // If we're in a batch, or in the commit phase, downgrade sync to task - return Task; - } - return expirationTime; - } - function scheduleErrorRecovery(fiber: Fiber) { - scheduleUpdateImpl(fiber, Task, true); + scheduleWorkImpl(fiber, Task, true); } function recalculateCurrentTime(): ExpirationTime { @@ -1590,8 +1592,7 @@ module.exports = function( function deferredUpdates(fn: () => A): A { const previousExpirationContext = expirationContext; - const currentTime = recalculateCurrentTime(); - expirationContext = asyncExpirationTime(currentTime); + expirationContext = computeAsyncExpiration(); try { return fn(); } finally { @@ -1600,8 +1601,9 @@ module.exports = function( } return { - scheduleUpdate: scheduleUpdate, - getExpirationTime: getExpirationTime, + computeAsyncExpiration: computeAsyncExpiration, + computeExpirationForFiber: computeExpirationForFiber, + scheduleWork: scheduleWork, batchedUpdates: batchedUpdates, unbatchedUpdates: unbatchedUpdates, flushSync: flushSync, diff --git a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js index 045e190efa77e..5da8671536fef 100644 --- a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js +++ b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js @@ -20,6 +20,7 @@ const {NoWork} = require('ReactFiberExpirationTime'); const {ClassComponent, HostRoot} = require('ReactTypeOfWork'); const invariant = require('fbjs/lib/invariant'); + if (__DEV__) { var warning = require('fbjs/lib/warning'); } @@ -31,14 +32,13 @@ type PartialState = // Callbacks are not validated until invocation type Callback = mixed; -export type Update = { +export type Update = { expirationTime: ExpirationTime, partialState: PartialState, callback: Callback | null, isReplace: boolean, isForced: boolean, - isTopLevelUnmount: boolean, - next: Update | null, + next: Update | null, }; // Singly linked-list of updates. When an update is scheduled, it is added to @@ -52,25 +52,31 @@ export type Update = { // The work-in-progress queue is always a subset of the current queue. // // When the tree is committed, the work-in-progress becomes the current. -export type UpdateQueue = { - first: Update | null, - last: Update | null, +export type UpdateQueue = { + // A processed update is not removed from the queue if there are any + // unprocessed updates that came before it. In that case, we need to keep + // track of the base state, which represents the base state of the first + // unprocessed update, which is the same as the first update in the list. + baseState: State, + // For the same reason, we keep track of the remaining expiration time. + expirationTime: ExpirationTime, + first: Update | null, + last: Update | null, + callbackList: Array> | null, hasForceUpdate: boolean, - callbackList: null | Array, // Dev only isProcessing?: boolean, }; -let _queue1; -let _queue2; - -function createUpdateQueue(): UpdateQueue { - const queue: UpdateQueue = { +function createUpdateQueue(baseState: State): UpdateQueue { + const queue: UpdateQueue = { + baseState, + expirationTime: NoWork, first: null, last: null, - hasForceUpdate: false, callbackList: null, + hasForceUpdate: false, }; if (__DEV__) { queue.isProcessing = false; @@ -78,120 +84,50 @@ function createUpdateQueue(): UpdateQueue { return queue; } -function cloneUpdate(update: Update): Update { - return { - expirationTime: update.expirationTime, - partialState: update.partialState, - callback: update.callback, - isReplace: update.isReplace, - isForced: update.isForced, - isTopLevelUnmount: update.isTopLevelUnmount, - next: null, - }; -} - -function insertUpdateIntoQueue( - queue: UpdateQueue, - update: Update, - insertAfter: Update | null, - insertBefore: Update | null, -) { - if (insertAfter !== null) { - insertAfter.next = update; - } else { - // This is the first item in the queue. - update.next = queue.first; - queue.first = update; - } - - if (insertBefore !== null) { - update.next = insertBefore; +function insertUpdateIntoQueue( + queue: UpdateQueue, + update: Update, +): void { + // Append the update to the end of the list. + if (queue.last === null) { + // Queue is empty + queue.first = queue.last = update; } else { - // This is the last item in the queue. + queue.last.next = update; queue.last = update; } -} - -// Returns the update after which the incoming update should be inserted into -// the queue, or null if it should be inserted at beginning. -function findInsertionPosition(queue, update): Update | null { - const expirationTime = update.expirationTime; - let insertAfter = null; - let insertBefore = null; - if (queue.last !== null && queue.last.expirationTime <= expirationTime) { - // Fast path for the common case where the update should be inserted at - // the end of the queue. - insertAfter = queue.last; - } else { - insertBefore = queue.first; - while ( - insertBefore !== null && - insertBefore.expirationTime <= expirationTime - ) { - insertAfter = insertBefore; - insertBefore = insertBefore.next; - } + if ( + queue.expirationTime === NoWork || + queue.expirationTime > update.expirationTime + ) { + queue.expirationTime = update.expirationTime; } - return insertAfter; } +exports.insertUpdateIntoQueue = insertUpdateIntoQueue; -function ensureUpdateQueues(fiber: Fiber) { +function insertUpdateIntoFiber( + fiber: Fiber, + update: Update, +): void { + // We'll have at least one and at most two distinct update queues. const alternateFiber = fiber.alternate; - let queue1 = fiber.updateQueue; if (queue1 === null) { - queue1 = fiber.updateQueue = createUpdateQueue(); + queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState); } let queue2; if (alternateFiber !== null) { queue2 = alternateFiber.updateQueue; if (queue2 === null) { - queue2 = alternateFiber.updateQueue = createUpdateQueue(); + queue2 = alternateFiber.updateQueue = createUpdateQueue( + alternateFiber.memoizedState, + ); } } else { queue2 = null; } - - _queue1 = queue1; - // Return null if there is no alternate queue, or if its queue is the same. - _queue2 = queue2 !== queue1 ? queue2 : null; -} - -// The work-in-progress queue is a subset of the current queue (if it exists). -// We need to insert the incoming update into both lists. However, it's possible -// that the correct position in one list will be different from the position in -// the other. Consider the following case: -// -// Current: 3-5-6 -// Work-in-progress: 6 -// -// Then we receive an update with priority 4 and insert it into each list: -// -// Current: 3-4-5-6 -// Work-in-progress: 4-6 -// -// In the current queue, the new update's `next` pointer points to the update -// with priority 5. But in the work-in-progress queue, the pointer points to the -// update with priority 6. Because these two queues share the same persistent -// data structure, this won't do. (This can only happen when the incoming update -// has higher priority than all the updates in the work-in-progress queue.) -// -// To solve this, in the case where the incoming update needs to be inserted -// into two different positions, we'll make a clone of the update and insert -// each copy into a separate queue. This forks the list while maintaining a -// persistent structure, because the update that is added to the work-in-progress -// is always added to the front of the list. -// -// However, if incoming update is inserted into the same position of both lists, -// we shouldn't make a copy. -// -// If the update is cloned, it returns the cloned update. -function insertUpdate(fiber: Fiber, update: Update): Update | null { - // We'll have at least one and at most two distinct update queues. - ensureUpdateQueues(fiber); - const queue1 = _queue1; - const queue2 = _queue2; + queue2 = queue2 !== queue1 ? queue2 : null; // Warn if an update is scheduled from inside an updater function. if (__DEV__) { @@ -206,161 +142,40 @@ function insertUpdate(fiber: Fiber, update: Update): Update | null { } } - // Find the insertion position in the first queue. - const insertAfter1 = findInsertionPosition(queue1, update); - const insertBefore1 = insertAfter1 !== null - ? insertAfter1.next - : queue1.first; - + // If there's only one queue, add the update to that queue and exit. if (queue2 === null) { - // If there's no alternate queue, there's nothing else to do but insert. - insertUpdateIntoQueue(queue1, update, insertAfter1, insertBefore1); - return null; + insertUpdateIntoQueue(queue1, update); + return; } - // If there is an alternate queue, find the insertion position. - const insertAfter2 = findInsertionPosition(queue2, update); - const insertBefore2 = insertAfter2 !== null - ? insertAfter2.next - : queue2.first; - - // Now we can insert into the first queue. This must come after finding both - // insertion positions because it mutates the list. - insertUpdateIntoQueue(queue1, update, insertAfter1, insertBefore1); - - // See if the insertion positions are equal. Be careful to only compare - // non-null values. - if ( - (insertBefore1 === insertBefore2 && insertBefore1 !== null) || - (insertAfter1 === insertAfter2 && insertAfter1 !== null) - ) { - // The insertion positions are the same, so when we inserted into the first - // queue, it also inserted into the alternate. All we need to do is update - // the alternate queue's `first` and `last` pointers, in case they - // have changed. - if (insertAfter2 === null) { - queue2.first = update; - } - if (insertBefore2 === null) { - queue2.last = null; - } - return null; - } else { - // The insertion positions are different, so we need to clone the update and - // insert the clone into the alternate queue. - const update2 = cloneUpdate(update); - insertUpdateIntoQueue(queue2, update2, insertAfter2, insertBefore2); - return update2; + // If either queue is empty, we need to add to both queues. + if (queue1.last === null || queue2.last === null) { + insertUpdateIntoQueue(queue1, update); + insertUpdateIntoQueue(queue2, update); + return; } -} - -function addUpdate( - fiber: Fiber, - partialState: PartialState | null, - callback: mixed, - expirationTime: ExpirationTime, -): void { - const update = { - expirationTime, - partialState, - callback, - isReplace: false, - isForced: false, - isTopLevelUnmount: false, - next: null, - }; - insertUpdate(fiber, update); -} -exports.addUpdate = addUpdate; -function addReplaceUpdate( - fiber: Fiber, - state: any | null, - callback: Callback | null, - expirationTime: ExpirationTime, -): void { - const update = { - expirationTime, - partialState: state, - callback, - isReplace: true, - isForced: false, - isTopLevelUnmount: false, - next: null, - }; - insertUpdate(fiber, update); + // If both lists are not empty, the last update is the same for both lists + // because of structural sharing. So, we should only append to one of + // the lists. + insertUpdateIntoQueue(queue1, update); + // But we still need to update the `last` pointer of queue2. + queue2.last = update; } -exports.addReplaceUpdate = addReplaceUpdate; - -function addForceUpdate( - fiber: Fiber, - callback: Callback | null, - expirationTime: ExpirationTime, -): void { - const update = { - expirationTime, - partialState: null, - callback, - isReplace: false, - isForced: true, - isTopLevelUnmount: false, - next: null, - }; - insertUpdate(fiber, update); -} -exports.addForceUpdate = addForceUpdate; +exports.insertUpdateIntoFiber = insertUpdateIntoFiber; function getUpdateExpirationTime(fiber: Fiber): ExpirationTime { - const updateQueue = fiber.updateQueue; - if (updateQueue === null) { + if (fiber.tag !== ClassComponent && fiber.tag !== HostRoot) { return NoWork; } - if (fiber.tag !== ClassComponent && fiber.tag !== HostRoot) { + const updateQueue = fiber.updateQueue; + if (updateQueue === null) { return NoWork; } - return updateQueue.first !== null ? updateQueue.first.expirationTime : NoWork; + return updateQueue.expirationTime; } exports.getUpdateExpirationTime = getUpdateExpirationTime; -function addTopLevelUpdate( - fiber: Fiber, - partialState: PartialState, - callback: Callback | null, - expirationTime: ExpirationTime, -): void { - const isTopLevelUnmount = partialState.element === null; - - const update = { - expirationTime, - partialState, - callback, - isReplace: false, - isForced: false, - isTopLevelUnmount, - next: null, - }; - const update2 = insertUpdate(fiber, update); - - if (isTopLevelUnmount) { - // TODO: Redesign the top-level mount/update/unmount API to avoid this - // special case. - const queue1 = _queue1; - const queue2 = _queue2; - - // Drop all updates that are lower-priority, so that the tree is not - // remounted. We need to do this for both queues. - if (queue1 !== null && update.next !== null) { - update.next = null; - queue1.last = update; - } - if (queue2 !== null && update2 !== null && update2.next !== null) { - update2.next = null; - queue2.last = update; - } - } -} -exports.addTopLevelUpdate = addTopLevelUpdate; - function getStateFromUpdate(update, instance, prevState, props) { const partialState = update.partialState; if (typeof partialState === 'function') { @@ -371,19 +186,20 @@ function getStateFromUpdate(update, instance, prevState, props) { } } -function beginUpdateQueue( +function processUpdateQueue( current: Fiber | null, workInProgress: Fiber, - queue: UpdateQueue, + queue: UpdateQueue, instance: any, - prevState: any, props: any, renderExpirationTime: ExpirationTime, -): any { +): State { if (current !== null && current.updateQueue === queue) { // We need to create a work-in-progress queue, by cloning the current queue. const currentQueue = queue; queue = workInProgress.updateQueue = { + baseState: currentQueue.baseState, + expirationTime: currentQueue.expirationTime, first: currentQueue.first, last: currentQueue.last, // These fields are no longer valid because they were already committed. @@ -399,24 +215,47 @@ function beginUpdateQueue( queue.isProcessing = true; } - // Calculate these using the the existing values as a base. - let callbackList = queue.callbackList; - let hasForceUpdate = queue.hasForceUpdate; + // Reset the remaining expiration time. If we skip over any updates, we'll + // increase this accordingly. + queue.expirationTime = NoWork; - // Applies updates with matching priority to the previous state to create - // a new state object. - let state = prevState; + let state = queue.baseState; let dontMutatePrevState = true; let update = queue.first; - while (update !== null && update.expirationTime <= renderExpirationTime) { - // Remove each update from the queue right before it is processed. That way - // if setState is called from inside an updater function, the new update - // will be inserted in the correct position. - queue.first = update.next; - if (queue.first === null) { - queue.last = null; + let didSkip = false; + while (update !== null) { + const updateExpirationTime = update.expirationTime; + if (updateExpirationTime > renderExpirationTime) { + // This update does not have sufficient priority. Skip it. + const remainingExpirationTime = queue.expirationTime; + if ( + remainingExpirationTime === NoWork || + remainingExpirationTime > updateExpirationTime + ) { + // Update the remaining expiration time. + queue.expirationTime = updateExpirationTime; + } + if (!didSkip) { + didSkip = true; + queue.baseState = state; + } + // Continue to the next update. + update = update.next; + continue; + } + + // This update does have sufficient priority. + + // If no previous updates were skipped, drop this update from the queue by + // advancing the head of the list. + if (!didSkip) { + queue.first = update.next; + if (queue.first === null) { + queue.last = null; + } } + // Process the update let partialState; if (update.isReplace) { state = getStateFromUpdate(update, instance, state, props); @@ -425,6 +264,7 @@ function beginUpdateQueue( partialState = getStateFromUpdate(update, instance, state, props); if (partialState) { if (dontMutatePrevState) { + // $FlowFixMe: Idk how to type this properly. state = Object.assign({}, state, partialState); } else { state = Object.assign(state, partialState); @@ -433,29 +273,31 @@ function beginUpdateQueue( } } if (update.isForced) { - hasForceUpdate = true; + queue.hasForceUpdate = true; } - // Second condition ignores top-level unmount callbacks if they are not the - // last update in the queue, since a subsequent update will cause a remount. - if ( - update.callback !== null && - !(update.isTopLevelUnmount && update.next !== null) - ) { - callbackList = callbackList !== null ? callbackList : []; - callbackList.push(update.callback); - workInProgress.effectTag |= CallbackEffect; + if (update.callback !== null) { + // Append to list of callbacks. + let callbackList = queue.callbackList; + if (callbackList === null) { + callbackList = queue.callbackList = []; + } + callbackList.push(update); } update = update.next; } - queue.callbackList = callbackList; - queue.hasForceUpdate = hasForceUpdate; - - if (queue.first === null && callbackList === null && !hasForceUpdate) { - // The queue is empty and there are no callbacks. We can reset it. + if (queue.callbackList !== null) { + workInProgress.effectTag |= CallbackEffect; + } else if (queue.first === null && !queue.hasForceUpdate) { + // The queue is empty. We can reset it. workInProgress.updateQueue = null; } + if (!didSkip) { + didSkip = true; + queue.baseState = state; + } + if (__DEV__) { // No longer processing. queue.isProcessing = false; @@ -463,23 +305,21 @@ function beginUpdateQueue( return state; } -exports.beginUpdateQueue = beginUpdateQueue; +exports.processUpdateQueue = processUpdateQueue; -function commitCallbacks( - finishedWork: Fiber, - queue: UpdateQueue, - context: mixed, -) { +function commitCallbacks(queue: UpdateQueue, context: any) { const callbackList = queue.callbackList; if (callbackList === null) { return; } - // Set the list to null to make sure they don't get called more than once. queue.callbackList = null; - for (let i = 0; i < callbackList.length; i++) { - const callback = callbackList[i]; + const update = callbackList[i]; + const callback = update.callback; + // This update might be processed again. Clear the callback so it's only + // called once. + update.callback = null; invariant( typeof callback === 'function', 'Invalid argument passed as callback. Expected a function. Instead ' + diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalScheduling-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalScheduling-test.js index 3d44622333097..1569f2bc98c42 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncrementalScheduling-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalScheduling-test.js @@ -57,11 +57,13 @@ describe('ReactIncrementalScheduling', () => { ReactNoop.render(); }); }); + // The sync updates flush first. + expect(ReactNoop.getChildren()).toEqual([span(4)]); - // The low pri update should be flushed last, even though it was scheduled - // before the sync updates. + // The terminal value should be the last update that was scheduled, + // regardless of priority. In this case, that's the last sync update. ReactNoop.flush(); - expect(ReactNoop.getChildren()).toEqual([span(5)]); + expect(ReactNoop.getChildren()).toEqual([span(4)]); }); it('schedules top-level updates with same priority in order of insertion', () => { diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js index 7acffd8991c54..bec1f128974b1 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js @@ -246,6 +246,7 @@ describe('ReactIncrementalTriangle', () => { simulate(step(1), flush(3), toggle(18), step(0)); simulate(step(4), flush(52), expire(1476), flush(17), step(0)); simulate(interrupt(), toggle(10), step(2), expire(990), flush(46)); + simulate(interrupt(), step(6), step(7), toggle(6), interrupt()); }); it('fuzz tester', () => { diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalUpdates-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalUpdates-test.js index 224cb0e2ddb74..0d00233462f4d 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncrementalUpdates-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalUpdates-test.js @@ -154,19 +154,20 @@ describe('ReactIncrementalUpdates', () => { expect(ReactNoop.getChildren()).toEqual([span('')]); // Schedule some more updates at different priorities{ - instance.setState(createUpdate('f')); + instance.setState(createUpdate('d')); ReactNoop.flushSync(() => { - instance.setState(createUpdate('d')); instance.setState(createUpdate('e')); + instance.setState(createUpdate('f')); }); instance.setState(createUpdate('g')); // The sync updates should have flushed, but not the async ones - expect(ReactNoop.getChildren()).toEqual([span('de')]); + expect(ReactNoop.getChildren()).toEqual([span('ef')]); - // Now flush the remaining work. Updates a, b, and c should be processed - // again, since they were interrupted last time. - expect(ReactNoop.flush()).toEqual(['a', 'b', 'c', 'f', 'g']); + // Now flush the remaining work. Even though e and f were already processed, + // they should be processed again, to ensure that the terminal state + // is deterministic. + expect(ReactNoop.flush()).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g']); expect(ReactNoop.getChildren()).toEqual([span('abcdefg')]); }); @@ -202,23 +203,24 @@ describe('ReactIncrementalUpdates', () => { expect(ReactNoop.getChildren()).toEqual([span('')]); // Schedule some more updates at different priorities{ - instance.setState(createUpdate('f')); + instance.setState(createUpdate('d')); ReactNoop.flushSync(() => { - instance.setState(createUpdate('d')); + instance.setState(createUpdate('e')); // No longer a public API, but we can test that it works internally by // reaching into the updater. - instance.updater.enqueueReplaceState(instance, createUpdate('e')); + instance.updater.enqueueReplaceState(instance, createUpdate('f')); }); instance.setState(createUpdate('g')); // The sync updates should have flushed, but not the async ones. Update d // was dropped and replaced by e. - expect(ReactNoop.getChildren()).toEqual([span('e')]); + expect(ReactNoop.getChildren()).toEqual([span('f')]); - // Now flush the remaining work. Updates a, b, and c should be processed - // again, since they were interrupted last time. - expect(ReactNoop.flush()).toEqual(['a', 'b', 'c', 'f', 'g']); - expect(ReactNoop.getChildren()).toEqual([span('abcefg')]); + // Now flush the remaining work. Even though e and f were already processed, + // they should be processed again, to ensure that the terminal state + // is deterministic. + expect(ReactNoop.flush()).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g']); + expect(ReactNoop.getChildren()).toEqual([span('fg')]); }); it('passes accumulation of previous updates to replaceState updater function', () => { diff --git a/yarn.lock b/yarn.lock index 76c2c27615f09..4cd5cfb7290c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2614,6 +2614,12 @@ istanbul-reports@^1.1.1: dependencies: handlebars "^4.0.3" +jasmine-check@^1.0.0-rc.0: + version "1.0.0-rc.0" + resolved "https://registry.yarnpkg.com/jasmine-check/-/jasmine-check-1.0.0-rc.0.tgz#117728c150078ecf211986c5f164275b71e937a4" + dependencies: + testcheck "^1.0.0-rc" + jest-changed-files@20.1.0-delta.1: version "20.1.0-delta.1" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-20.1.0-delta.1.tgz#912f8eff09c79b28fc7b66513f0ad505f1b378d5" @@ -4316,6 +4322,10 @@ test-exclude@^4.0.3: read-pkg-up "^1.0.1" require-main-filename "^1.0.1" +testcheck@^1.0.0-rc: + version "1.0.0-rc.2" + resolved "https://registry.yarnpkg.com/testcheck/-/testcheck-1.0.0-rc.2.tgz#11356a25b84575efe0b0857451e85b5fa74ee4e4" + text-table@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"