From 24b7272525a115c087d5e5ef6d47e4604a081905 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 18 Aug 2017 16:08:34 -0700 Subject: [PATCH] Initial version of blocking & top-level resuming Refactors the complete phase to add support for blocking a tree from committing. Also adds a basic version of "resuming" for HostRoots. Neither of these features are actually implemented in this commit; will come later. --- .../shared/fiber/ReactFiberBeginWork.js | 10 +++ src/renderers/shared/fiber/ReactFiberRoot.js | 15 ++++ .../shared/fiber/ReactFiberScheduler.js | 75 ++++++++++++++++--- 3 files changed, 91 insertions(+), 9 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 5d49bf7e89dd7..384a0da4db8a8 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -332,6 +332,16 @@ module.exports = function( function updateHostRoot(current, workInProgress, renderExpirationTime) { const root = (workInProgress.stateNode: FiberRoot); pushHostRootContext(workInProgress); + if (root.completedAt === renderExpirationTime) { + // The root is already complete. Bail out and commit. + // TODO: This is a limited version of resuming that only applies to + // the root, to account for the pathological case where a completed + // root must be completely restarted before it can commit. Once we + // implement resuming for real, this special branch shouldn't + // be neccessary. + return null; + } + const updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { const prevState = workInProgress.memoizedState; diff --git a/src/renderers/shared/fiber/ReactFiberRoot.js b/src/renderers/shared/fiber/ReactFiberRoot.js index f78205d2bcae5..37b7950e9c5d3 100644 --- a/src/renderers/shared/fiber/ReactFiberRoot.js +++ b/src/renderers/shared/fiber/ReactFiberRoot.js @@ -11,8 +11,10 @@ 'use strict'; import type {Fiber} from 'ReactFiber'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; const {createHostRootFiber} = require('ReactFiber'); +const {Done} = require('ReactFiberExpirationTime'); export type FiberRoot = { // Any additional information from the host associated with this root. @@ -21,6 +23,8 @@ export type FiberRoot = { current: Fiber, // Determines if this root has already been added to the schedule for work. isScheduled: boolean, + // The time at which this root completed. + completedAt: ExpirationTime, // The work schedule is a linked list. nextScheduledRoot: FiberRoot | null, // Top context object, used by renderSubtreeIntoContainer @@ -28,6 +32,16 @@ export type FiberRoot = { pendingContext: Object | null, }; +// Indicates whether the root is blocked from committing at a particular +// expiration time. +exports.isRootBlocked = function( + root: FiberRoot, + time: ExpirationTime, +): boolean { + // TODO: Implementation + return false; +}; + exports.createFiberRoot = function(containerInfo: any): FiberRoot { // Cyclic construction. This cheats the type system right now because // stateNode is any. @@ -36,6 +50,7 @@ exports.createFiberRoot = function(containerInfo: any): FiberRoot { current: uninitializedFiber, containerInfo: containerInfo, isScheduled: false, + completedAt: Done, nextScheduledRoot: null, context: null, pendingContext: null, diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 5b64cbfc063bf..f991c3bb5ec59 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -52,6 +52,7 @@ var {ReactCurrentOwner} = require('ReactGlobalSharedState'); var getComponentName = require('getComponentName'); var {createWorkInProgress} = require('ReactFiber'); +var {isRootBlocked} = require('ReactFiberRoot'); var {onCommitRoot} = require('ReactFiberDevToolsHook'); var { @@ -260,10 +261,11 @@ module.exports = function( } function resetNextUnitOfWork() { - // Clear out roots with no more work on them, or if they have uncaught errors + // Clear out roots with no more work on them while ( nextScheduledRoot !== null && - nextScheduledRoot.current.expirationTime === Done + nextScheduledRoot.current.expirationTime === Done && + nextScheduledRoot.completedAt === Done ) { // Unschedule this root. nextScheduledRoot.isScheduled = false; @@ -287,10 +289,11 @@ module.exports = function( let earliestExpirationRoot = null; let earliestExpirationTime = Done; while (root !== null) { + let rootExpirationTime = shouldWorkOnRoot(root); if ( - root.current.expirationTime !== Done && + rootExpirationTime !== Done && (earliestExpirationTime === Done || - earliestExpirationTime > root.current.expirationTime) + earliestExpirationTime > rootExpirationTime) ) { earliestExpirationTime = root.current.expirationTime; earliestExpirationRoot = root; @@ -307,10 +310,26 @@ module.exports = function( // unfortunately this is it. resetContextStack(); - nextUnitOfWork = createWorkInProgress( - earliestExpirationRoot.current, - earliestExpirationTime, - ); + if (earliestExpirationRoot.completedAt === nextRenderExpirationTime) { + // If the root is already complete, reuse the existing work-in-progress. + // TODO: This is a limited version of resuming that only applies to + // the root, to account for the pathological case where a completed + // root must be completely restarted before it can commit. Once we + // implement resuming for real, this special branch shouldn't + // be neccessary. + nextUnitOfWork = earliestExpirationRoot.current.alternate; + invariant( + nextUnitOfWork !== null, + 'Expected a completed root to have a work-in-progress. This error ' + + 'is likely caused by a bug in React. Please file an issue.', + ); + } else { + earliestExpirationRoot.completedAt = Done; + nextUnitOfWork = createWorkInProgress( + earliestExpirationRoot.current, + earliestExpirationTime, + ); + } if (earliestExpirationRoot !== nextRenderedTree) { // We've switched trees. Reset the nested update counter. nestedUpdateCount = 0; @@ -325,6 +344,37 @@ module.exports = function( return; } + // Indicates whether the root should be worked on. Not the same as whether a + // root has work, because work could be blocked. + function shouldWorkOnRoot(root: FiberRoot): ExpirationTime { + const completedAt = root.completedAt; + const expirationTime = root.current.expirationTime; + + if (expirationTime === Done) { + // There's no work in this tree. + return Done; + } + + if (completedAt !== Done) { + // The root completed but was blocked from committing. + + if (expirationTime < completedAt) { + // We have work that expires earlier than the completed root. Regardless + // of whether the root is blocked, we should work on it. + return expirationTime; + } + + // There have been no higher priority updates since we completed the root. + // If it's still blocked, return Done, as if it has no more work. If it's + // no longer blocked, return the time at which it completed so that we + // can commit it. + const isBlocked = isRootBlocked(root, expirationTime); + return isBlocked ? Done : completedAt; + } + + return expirationTime; + } + function commitAllHostEffects() { while (nextEffect !== null) { if (__DEV__) { @@ -450,6 +500,8 @@ module.exports = function( 'in React. Please file an issue.', ); + root.completedAt = Done; + if (nextRenderExpirationTime <= mostRecentCurrentTime) { // Keep track of the number of iterations to prevent an infinite // update loop. @@ -705,7 +757,12 @@ module.exports = function( // We've reached the root. Mark the root as pending commit. Depending // on how much time we have left, we'll either commit it now or in // the next frame. - pendingCommit = workInProgress; + const root = workInProgress.stateNode; + if (isRootBlocked(root, nextRenderExpirationTime)) { + root.completedAt = workInProgress.expirationTime = nextRenderExpirationTime; + } else { + pendingCommit = workInProgress; + } return null; } }