From 0911da3f8edb804dd2de69881bc657ff114181fa Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 23 Feb 2018 15:28:56 -0800 Subject: [PATCH] Coalesce like-priority updates made to the same component Maybe it's ok to do this across components? It would make this much simpler. --- .../src/ReactFiberBeginWork.js | 13 +- .../src/ReactFiberClassComponent.js | 31 +++-- .../src/ReactFiberCommitWork.js | 17 ++- .../src/ReactFiberReconciler.js | 84 ++++++------- .../src/ReactFiberScheduler.js | 117 ++++++++++++------ .../src/ReactFiberUnwindWork.js | 13 +- .../src/ReactFiberUpdateQueue.js | 24 ++++ .../src/ReactPriorityLevel.js | 16 +++ .../ReactExpiration-test.internal.js | 103 +++++++++++++++ .../src/__tests__/ReactExpiration-test.js | 45 ------- 10 files changed, 316 insertions(+), 147 deletions(-) create mode 100644 packages/react-reconciler/src/ReactPriorityLevel.js create mode 100644 packages/react-reconciler/src/__tests__/ReactExpiration-test.internal.js delete mode 100644 packages/react-reconciler/src/__tests__/ReactExpiration-test.js diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index e4a82257e6..154d57f4b6 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -14,6 +14,7 @@ import type {HostContext} from './ReactFiberHostContext'; import type {HydrationContext} from './ReactFiberHydrationContext'; import type {FiberRoot} from './ReactFiberRoot'; import type {ExpirationTime} from './ReactFiberExpirationTime'; +import type {PriorityLevel} from './ReactPriorityLevel'; import { IndeterminateComponent, @@ -92,11 +93,12 @@ export default function( startTime: ExpirationTime, expirationTime: ExpirationTime, ) => void, - computeExpirationForFiber: ( - startTime: ExpirationTime, - fiber: Fiber, - ) => ExpirationTime, + computeUpdatePriorityForFiber: (fiber: Fiber) => PriorityLevel, recalculateCurrentTime: () => ExpirationTime, + computeExpirationTimeForPriority: ( + priorityLevel: PriorityLevel, + startTime: ExpirationTime, + ) => ExpirationTime, ) { const {shouldSetTextContent, shouldDeprioritizeSubtree} = config; @@ -117,10 +119,11 @@ export default function( updateClassInstance, } = ReactFiberClassComponent( scheduleWork, - computeExpirationForFiber, + computeUpdatePriorityForFiber, memoizeProps, memoizeState, recalculateCurrentTime, + computeExpirationTimeForPriority, ); // TODO: Remove this and use reconcileChildrenAtExpirationTime directly. diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 1deca2aac3..98d8668e4e 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -10,6 +10,7 @@ import type {Fiber} from './ReactFiber'; import type {ExpirationTime} from './ReactFiberExpirationTime'; import type {CapturedValue} from './ReactCapturedValue'; +import type {PriorityLevel} from './ReactPriorityLevel'; import {Update} from 'shared/ReactTypeOfSideEffect'; import { @@ -115,13 +116,14 @@ export default function( startTime: ExpirationTime, expirationTime: ExpirationTime, ) => void, - computeExpirationForFiber: ( - startTime: ExpirationTime, - fiber: Fiber, - ) => ExpirationTime, + computeUpdatePriorityForFiber: (fiber: Fiber) => PriorityLevel, memoizeProps: (workInProgress: Fiber, props: any) => void, memoizeState: (workInProgress: Fiber, state: any) => void, recalculateCurrentTime: () => ExpirationTime, + computeExpirationTimeForPriority: ( + priorityLevel: PriorityLevel, + startTime: ExpirationTime, + ) => ExpirationTime, ) { // Class component state updater const updater = { @@ -132,10 +134,15 @@ export default function( if (__DEV__) { warnOnInvalidCallback(callback, 'setState'); } + const priorityLevel = computeUpdatePriorityForFiber(fiber); const currentTime = recalculateCurrentTime(); - const expirationTime = computeExpirationForFiber(currentTime, fiber); + const expirationTime = computeExpirationTimeForPriority( + priorityLevel, + currentTime, + ); const update = { expirationTime, + priorityLevel, partialState, callback, isReplace: false, @@ -152,10 +159,15 @@ export default function( if (__DEV__) { warnOnInvalidCallback(callback, 'replaceState'); } + const priorityLevel = computeUpdatePriorityForFiber(fiber); const currentTime = recalculateCurrentTime(); - const expirationTime = computeExpirationForFiber(currentTime, fiber); + const expirationTime = computeExpirationTimeForPriority( + priorityLevel, + currentTime, + ); const update = { expirationTime, + priorityLevel, partialState: state, callback, isReplace: true, @@ -172,11 +184,16 @@ export default function( if (__DEV__) { warnOnInvalidCallback(callback, 'forceUpdate'); } + const priorityLevel = computeUpdatePriorityForFiber(fiber); const currentTime = recalculateCurrentTime(); - const expirationTime = computeExpirationForFiber(currentTime, fiber); + const expirationTime = computeExpirationTimeForPriority( + priorityLevel, + currentTime, + ); const update = { expirationTime, partialState: null, + priorityLevel, callback, isReplace: false, isForced: true, diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index a327977f58..880921b1d7 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -12,6 +12,7 @@ import type {Fiber} from './ReactFiber'; import type {FiberRoot} from './ReactFiber'; import type {ExpirationTime} from './ReactFiberExpirationTime'; import type {CapturedValue, CapturedError} from './ReactCapturedValue'; +import type {PriorityLevel} from './ReactPriorityLevel'; import { enableMutatingReconciler, @@ -92,12 +93,13 @@ export default function( startTime: ExpirationTime, expirationTime: ExpirationTime, ) => void, - computeExpirationForFiber: ( - startTime: ExpirationTime, - fiber: Fiber, - ) => ExpirationTime, + computeUpdatePriorityForFiber: (fiber: Fiber) => PriorityLevel, markLegacyErrorBoundaryAsFailed: (instance: mixed) => void, recalculateCurrentTime: () => ExpirationTime, + computeExpirationTimeForPriority: ( + priorityLevel: PriorityLevel, + startTime: ExpirationTime, + ) => ExpirationTime, ) { const {getPublicInstance, mutation, persistence} = config; @@ -156,10 +158,15 @@ export default function( } function scheduleExpirationBoundaryRecovery(fiber) { + const priorityLevel = computeUpdatePriorityForFiber(fiber); const currentTime = recalculateCurrentTime(); - const expirationTime = computeExpirationForFiber(currentTime, fiber); + const expirationTime = computeExpirationTimeForPriority( + priorityLevel, + currentTime, + ); const update = { expirationTime, + priorityLevel, partialState: false, callback: null, isReplace: true, diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 91cf36f9e0..1b937a01c3 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -11,6 +11,7 @@ import type {Fiber} from './ReactFiber'; import type {FiberRoot} from './ReactFiberRoot'; import type {ReactNodeList} from 'shared/ReactTypes'; import type {ExpirationTime} from './ReactFiberExpirationTime'; +import type {PriorityLevel} from './ReactPriorityLevel'; import { findCurrentHostFiber, @@ -33,6 +34,7 @@ import ReactFiberScheduler from './ReactFiberScheduler'; import {insertUpdateIntoFiber} from './ReactFiberUpdateQueue'; import ReactFiberInstrumentation from './ReactFiberInstrumentation'; import ReactDebugCurrentFiber from './ReactDebugCurrentFiber'; +import {NoPriority} from './ReactPriorityLevel'; let didWarnAboutNestedUpdates; @@ -296,7 +298,8 @@ export default function( const { computeUniqueAsyncExpiration, recalculateCurrentTime, - computeExpirationForFiber, + computeUpdatePriorityForFiber, + computeExpirationTimeForPriority, scheduleWork, requestWork, flushRoot, @@ -310,13 +313,37 @@ export default function( flushInteractiveUpdates, } = ReactFiberScheduler(config); - function scheduleRootUpdate( - current: Fiber, + function updateContainerAtExpirationTime( element: ReactNodeList, + container: OpaqueRoot, + parentComponent: ?React$Component, currentTime: ExpirationTime, + priorityLevel: PriorityLevel, expirationTime: ExpirationTime, callback: ?Function, ) { + // TODO: If this is a nested container, this won't be the root. + const current = container.current; + + if (__DEV__) { + if (ReactFiberInstrumentation.debugTool) { + if (current.alternate === null) { + ReactFiberInstrumentation.debugTool.onMountContainer(container); + } else if (element === null) { + ReactFiberInstrumentation.debugTool.onUnmountContainer(container); + } else { + ReactFiberInstrumentation.debugTool.onUpdateContainer(container); + } + } + } + + const context = getContextForSubtree(parentComponent); + if (container.context === null) { + container.context = context; + } else { + container.pendingContext = context; + } + if (__DEV__) { if ( ReactDebugCurrentFiber.phase === 'render' && @@ -347,6 +374,7 @@ export default function( const update = { expirationTime, + priorityLevel, partialState: {element}, callback, isReplace: false, @@ -360,45 +388,6 @@ export default function( return expirationTime; } - function updateContainerAtExpirationTime( - element: ReactNodeList, - container: OpaqueRoot, - parentComponent: ?React$Component, - currentTime: ExpirationTime, - expirationTime: ExpirationTime, - callback: ?Function, - ) { - // TODO: If this is a nested container, this won't be the root. - const current = container.current; - - if (__DEV__) { - if (ReactFiberInstrumentation.debugTool) { - if (current.alternate === null) { - ReactFiberInstrumentation.debugTool.onMountContainer(container); - } else if (element === null) { - ReactFiberInstrumentation.debugTool.onUnmountContainer(container); - } else { - ReactFiberInstrumentation.debugTool.onUpdateContainer(container); - } - } - } - - const context = getContextForSubtree(parentComponent); - if (container.context === null) { - container.context = context; - } else { - container.pendingContext = context; - } - - return scheduleRootUpdate( - current, - element, - currentTime, - expirationTime, - callback, - ); - } - function findHostInstance(fiber: Fiber): PI | null { const hostFiber = findCurrentHostFiber(fiber); if (hostFiber === null) { @@ -423,13 +412,18 @@ export default function( callback: ?Function, ): ExpirationTime { const current = container.current; + const priorityLevel = computeUpdatePriorityForFiber(current); const currentTime = recalculateCurrentTime(); - const expirationTime = computeExpirationForFiber(currentTime, current); + const expirationTime = computeExpirationTimeForPriority( + priorityLevel, + currentTime, + ); return updateContainerAtExpirationTime( element, container, parentComponent, currentTime, + priorityLevel, expirationTime, callback, ); @@ -442,12 +436,16 @@ export default function( expirationTime, callback, ) { + // TODO: Rethink this API. It's only used by the createBatch() API. Need + // to revisit that implementation once suspenders are implemented. + const priorityLevel = NoPriority; const currentTime = recalculateCurrentTime(); return updateContainerAtExpirationTime( element, container, parentComponent, currentTime, + priorityLevel, expirationTime, callback, ); diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 232ab4815f..8847902294 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -12,6 +12,7 @@ import type {Fiber} from './ReactFiber'; import type {FiberRoot, Batch} from './ReactFiberRoot'; import type {HydrationContext} from './ReactFiberHydrationContext'; import type {ExpirationTime} from './ReactFiberExpirationTime'; +import type {PriorityLevel} from './ReactPriorityLevel'; import ReactErrorUtils from 'shared/ReactErrorUtils'; import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState'; @@ -80,6 +81,13 @@ import { startCommitLifeCyclesTimer, stopCommitLifeCyclesTimer, } from './ReactDebugFiberPerf'; +import { + NoPriority, + RenderPriority, + SyncPriority, + DeferredPriority, + InteractivePriority, +} from './ReactPriorityLevel'; import {reset} from './ReactFiberStack'; import {createWorkInProgress} from './ReactFiber'; import {onCommitRoot} from './ReactFiberDevToolsHook'; @@ -180,8 +188,9 @@ export default function( hostContext, hydrationContext, scheduleWork, - computeExpirationForFiber, + computeUpdatePriorityForFiber, recalculateCurrentTime, + computeExpirationTimeForPriority, ); const {completeWork} = ReactFiberCompleteWork( config, @@ -208,9 +217,10 @@ export default function( config, onCommitPhaseError, scheduleWork, - computeExpirationForFiber, + computeUpdatePriorityForFiber, markLegacyErrorBoundaryAsFailed, recalculateCurrentTime, + computeExpirationTimeForPriority, ); const { now, @@ -228,10 +238,10 @@ export default function( // Used to ensure computeUniqueAsyncExpiration is monotonically increases. let lastUniqueAsyncExpiration: number = 0; - // Represents the expiration time that incoming updates should use. (If this - // is NoWork, use the default strategy: async updates in async mode, sync + // Represents the priority that incoming updates should use. (If this is + // NoPriority, use the default strategy: async updates in async mode, sync // updates in sync mode.) - let expirationContext: ExpirationTime = NoWork; + let priorityContext: PriorityLevel = NoPriority; let isWorking: boolean = false; @@ -990,6 +1000,7 @@ export default function( sourceFiber, boundaryFiber, value, + priorityLevel, startTime, expirationTime, ) { @@ -997,6 +1008,7 @@ export default function( const capturedValue = createCapturedValue(value, sourceFiber); const update = { expirationTime, + priorityLevel, partialState: null, callback: null, isReplace: false, @@ -1011,6 +1023,7 @@ export default function( function dispatch( sourceFiber: Fiber, value: mixed, + priorityLevel: PriorityLevel, startTime: ExpirationTime, expirationTime: ExpirationTime, ) { @@ -1036,6 +1049,7 @@ export default function( sourceFiber, fiber, value, + priorityLevel, startTime, expirationTime, ); @@ -1044,7 +1058,14 @@ export default function( break; // TODO: Handle async boundaries case HostRoot: - scheduleCapture(sourceFiber, fiber, value, startTime, expirationTime); + scheduleCapture( + sourceFiber, + fiber, + value, + priorityLevel, + startTime, + expirationTime, + ); return; } fiber = fiber.return; @@ -1057,6 +1078,7 @@ export default function( sourceFiber, sourceFiber, value, + priorityLevel, startTime, expirationTime, ); @@ -1065,7 +1087,7 @@ export default function( function onCommitPhaseError(fiber: Fiber, error: mixed) { const startTime = recalculateCurrentTime(); - return dispatch(fiber, error, startTime, Sync); + return dispatch(fiber, error, SyncPriority, startTime, Sync); } function computeAsyncExpiration(currentTime: ExpirationTime) { @@ -1098,23 +1120,20 @@ export default function( return lastUniqueAsyncExpiration; } - function computeExpirationForFiber( - currentTime: ExpirationTime, - fiber: Fiber, - ) { - let expirationTime; - if (expirationContext !== NoWork) { - // An explicit expiration context was set; - expirationTime = expirationContext; + function computeUpdatePriorityForFiber(fiber: Fiber): PriorityLevel { + let priorityLevel; + if (priorityContext !== NoPriority) { + // An explicit priority context was set; + priorityLevel = priorityContext; } else if (isWorking) { if (isCommitting) { // Updates that occur during the commit phase should have sync priority // by default. - expirationTime = Sync; + priorityLevel = SyncPriority; } else { // Updates during the render phase should expire at the same time as // the work that is being rendered. - expirationTime = nextRenderExpirationTime; + priorityLevel = RenderPriority; } } else { // No explicit expiration context was set, and we're not currently @@ -1122,28 +1141,36 @@ export default function( if (fiber.mode & AsyncMode) { if (isBatchingInteractiveUpdates) { // This is an interactive update - expirationTime = computeInteractiveExpiration(currentTime); + priorityLevel = InteractivePriority; } else { // This is an async update - expirationTime = computeAsyncExpiration(currentTime); + priorityLevel = DeferredPriority; } } else { // This is a sync update - expirationTime = Sync; + priorityLevel = SyncPriority; } } - if (isBatchingInteractiveUpdates) { - // This is an interactive update. Keep track of the lowest pending - // interactive expiration time. This allows us to synchronously flush - // all interactive updates when needed. - if ( - lowestPendingInteractiveExpirationTime === NoWork || - expirationTime > lowestPendingInteractiveExpirationTime - ) { - lowestPendingInteractiveExpirationTime = expirationTime; - } + return priorityLevel; + } + + function computeExpirationTimeForPriority( + priorityLevel: PriorityLevel, + startTime: ExpirationTime, + ) { + switch (priorityLevel) { + case NoPriority: + return nextRenderExpirationTime; + case SyncPriority: + return Sync; + case RenderPriority: + return nextRenderExpirationTime; + case InteractivePriority: + return computeInteractiveExpiration(startTime); + case DeferredPriority: + default: + return computeAsyncExpiration(startTime); } - return expirationTime; } function retryOnPromiseResolution( @@ -1193,6 +1220,18 @@ export default function( } } + if (isBatchingInteractiveUpdates) { + // This is an interactive update. Keep track of the lowest pending + // interactive expiration time. This allows us to synchronously flush + // all interactive updates when needed. + if ( + lowestPendingInteractiveExpirationTime === NoWork || + expirationTime > lowestPendingInteractiveExpirationTime + ) { + lowestPendingInteractiveExpirationTime = expirationTime; + } + } + let node = fiber; while (node !== null) { // Walk the parent path to the root and update each node's @@ -1262,13 +1301,12 @@ export default function( } function deferredUpdates(fn: () => A): A { - const previousExpirationContext = expirationContext; - const currentTime = recalculateCurrentTime(); - expirationContext = computeAsyncExpiration(currentTime); + const previousPriorityContext = priorityContext; + priorityContext = DeferredPriority; try { return fn(); } finally { - expirationContext = previousExpirationContext; + priorityContext = previousPriorityContext; } } function syncUpdates( @@ -1278,12 +1316,12 @@ export default function( c: C0, d: D, ): R { - const previousExpirationContext = expirationContext; - expirationContext = Sync; + const previousPriorityContext = priorityContext; + priorityContext = SyncPriority; try { return fn(a, b, c, d); } finally { - expirationContext = previousExpirationContext; + priorityContext = previousPriorityContext; } } @@ -1824,7 +1862,8 @@ export default function( return { recalculateCurrentTime, - computeExpirationForFiber, + computeUpdatePriorityForFiber, + computeExpirationTimeForPriority, scheduleWork, requestWork, flushRoot, diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index 2941c86fd4..619749d036 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -29,6 +29,7 @@ import { ShouldCapture, } from 'shared/ReactTypeOfSideEffect'; import {Sync} from './ReactFiberExpirationTime'; +import {NoPriority} from './ReactPriorityLevel'; import {enableGetDerivedStateFromCatch} from 'shared/ReactFeatureFlags'; @@ -85,9 +86,10 @@ export default function( renderStartTime, renderExpirationTime, ) { - const slightlyHigherPriority = renderExpirationTime - 1; + const slightlyEarlierExpirationTime = renderExpirationTime - 1; const loadingUpdate = { - expirationTime: slightlyHigherPriority, + expirationTime: slightlyEarlierExpirationTime, + priorityLevel: NoPriority, partialState: true, callback: null, isReplace: true, @@ -99,6 +101,7 @@ export default function( const revertUpdate = { expirationTime: renderExpirationTime, + priorityLevel: NoPriority, partialState: false, callback: null, isReplace: true, @@ -107,7 +110,11 @@ export default function( next: null, }; insertUpdateIntoFiber(workInProgress, revertUpdate); - scheduleWork(workInProgress, renderStartTime, slightlyHigherPriority); + scheduleWork( + workInProgress, + renderStartTime, + slightlyEarlierExpirationTime, + ); return false; } diff --git a/packages/react-reconciler/src/ReactFiberUpdateQueue.js b/packages/react-reconciler/src/ReactFiberUpdateQueue.js index 86677d0696..1919176efe 100644 --- a/packages/react-reconciler/src/ReactFiberUpdateQueue.js +++ b/packages/react-reconciler/src/ReactFiberUpdateQueue.js @@ -9,6 +9,7 @@ import type {Fiber} from './ReactFiber'; import type {ExpirationTime} from './ReactFiberExpirationTime'; +import type {PriorityLevel} from './ReactPriorityLevel'; import type {CapturedValue} from './ReactCapturedValue'; import { @@ -26,6 +27,7 @@ import warning from 'fbjs/lib/warning'; import {StrictMode} from './ReactTypeOfMode'; import {NoWork} from './ReactFiberExpirationTime'; +import {NoPriority} from './ReactPriorityLevel'; let didWarnUpdateInsideUpdate; @@ -42,6 +44,7 @@ type Callback = mixed; export type Update = { expirationTime: ExpirationTime, + priorityLevel: PriorityLevel, partialState: PartialState, callback: Callback | null, isReplace: boolean, @@ -101,6 +104,27 @@ export function insertUpdateIntoQueue( queue: UpdateQueue, update: Update, ): void { + const priorityLevel = update.priorityLevel; + if ( + priorityLevel !== NoPriority && + queue.expirationTime <= update.expirationTime + ) { + let node = queue.first; + let latestExpirationTimeWithMatchingPriority = NoWork; + while (node !== null) { + if ( + node.priorityLevel === priorityLevel && + node.expirationTime > latestExpirationTimeWithMatchingPriority + ) { + latestExpirationTimeWithMatchingPriority = node.expirationTime; + } + node = node.next; + } + if (latestExpirationTimeWithMatchingPriority !== NoWork) { + update.expirationTime = latestExpirationTimeWithMatchingPriority; + } + } + // Append the update to the end of the list. if (queue.last === null) { // Queue is empty diff --git a/packages/react-reconciler/src/ReactPriorityLevel.js b/packages/react-reconciler/src/ReactPriorityLevel.js new file mode 100644 index 0000000000..ddfc486479 --- /dev/null +++ b/packages/react-reconciler/src/ReactPriorityLevel.js @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export type PriorityLevel = 0 | 1 | 2 | 3 | 4; + +export const NoPriority = 0; +export const RenderPriority = 1; +export const SyncPriority = 2; +export const InteractivePriority = 3; +export const DeferredPriority = 4; diff --git a/packages/react-reconciler/src/__tests__/ReactExpiration-test.internal.js b/packages/react-reconciler/src/__tests__/ReactExpiration-test.internal.js new file mode 100644 index 0000000000..0d09cc8684 --- /dev/null +++ b/packages/react-reconciler/src/__tests__/ReactExpiration-test.internal.js @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @jest-environment node + */ + +'use strict'; + +let React; +let Fragment; +let ReactNoop; +let ReactFeatureFlags; + +describe('ReactExpiration', () => { + beforeEach(() => { + jest.resetModules(); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; + React = require('react'); + Fragment = React.Fragment; + ReactNoop = require('react-noop-renderer'); + }); + + function span(prop) { + return {type: 'span', children: [], prop}; + } + + it('increases priority of updates as time progresses', () => { + ReactNoop.render(); + + expect(ReactNoop.getChildren()).toEqual([]); + + // Nothing has expired yet because time hasn't advanced. + ReactNoop.flushExpired(); + expect(ReactNoop.getChildren()).toEqual([]); + + // Advance time a bit, but not enough to expire the low pri update. + ReactNoop.expire(4500); + ReactNoop.flushExpired(); + expect(ReactNoop.getChildren()).toEqual([]); + + // Advance by another second. Now the update should expire and flush. + ReactNoop.expire(1000); + ReactNoop.flushExpired(); + expect(ReactNoop.getChildren()).toEqual([span('done')]); + }); + + it('coalesces updates to the same component', () => { + class Foo extends React.Component { + state = {step: 0}; + componentDidUpdate() { + ReactNoop.yield(`Did update ${this.props.label}: ${this.state.step}`); + } + render() { + ReactNoop.yield(`Render ${this.props.label}: ${this.state.step}`); + return ; + } + } + + let a = React.createRef(); + let b = React.createRef(); + ReactNoop.render( + + + + , + ); + ReactNoop.flush(); + + a.value.setState({step: 1}); + + // Advance time to move into a new expiration bucket + ReactNoop.expire(2000); + + // Update A again. This update should coalesce with the previous update. + a.value.setState({step: 2}); + // Update B. This is the first update, so it has nothing to coalesce with. + b.value.setState({step: 2}); + + // Advance time by enough to expire step 1, but not step 2. + ReactNoop.expire(4000); + expect(ReactNoop.flushExpired()).toEqual([ + // Even though we called setState on A twice, both updates should flush in + // a single batch. + 'Render A: 2', + 'Did update A: 2', + // Update B has not expired yet, even though its setState was scheduled + // at the same time as A + ]); + expect(ReactNoop.getChildren()).toEqual([span('A: 2'), span('B: 0')]); + + // Now expire B, too. + ReactNoop.expire(2000); + expect(ReactNoop.flushExpired()).toEqual([ + 'Render B: 2', + 'Did update B: 2', + ]); + expect(ReactNoop.getChildren()).toEqual([span('A: 2'), span('B: 2')]); + }); +}); diff --git a/packages/react-reconciler/src/__tests__/ReactExpiration-test.js b/packages/react-reconciler/src/__tests__/ReactExpiration-test.js deleted file mode 100644 index 58d501cbfd..0000000000 --- a/packages/react-reconciler/src/__tests__/ReactExpiration-test.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) 2013-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @jest-environment node - */ - -'use strict'; - -let React; -let ReactNoop; - -describe('ReactExpiration', () => { - beforeEach(() => { - jest.resetModules(); - React = require('react'); - ReactNoop = require('react-noop-renderer'); - }); - - function span(prop) { - return {type: 'span', children: [], prop}; - } - - it('increases priority of updates as time progresses', () => { - ReactNoop.render(); - - expect(ReactNoop.getChildren()).toEqual([]); - - // Nothing has expired yet because time hasn't advanced. - ReactNoop.flushExpired(); - expect(ReactNoop.getChildren()).toEqual([]); - - // Advance time a bit, but not enough to expire the low pri update. - ReactNoop.expire(4500); - ReactNoop.flushExpired(); - expect(ReactNoop.getChildren()).toEqual([]); - - // Advance by another second. Now the update should expire and flush. - ReactNoop.expire(1000); - ReactNoop.flushExpired(); - expect(ReactNoop.getChildren()).toEqual([span('done')]); - }); -});