From 7912baea699af8a648bcbc0d739aaabfa1078e8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 7 Jun 2016 14:30:07 -0700 Subject: [PATCH] Merge pull request #6981 from sebmarkbage/newreconciler [Fiber] Add support for simple updates and fiber pooling (cherry picked from commit ccd26ee020852533a8342880d8b7a662bfe55678) --- src/renderers/shared/fiber/ReactChildFiber.js | 37 ++++++- src/renderers/shared/fiber/ReactFiber.js | 80 +++++++++++++--- .../shared/fiber/ReactFiberBeginWork.js | 96 +++++++++++-------- .../shared/fiber/ReactFiberCompleteWork.js | 41 ++++---- .../shared/fiber/ReactFiberReconciler.js | 35 +++++-- .../fiber/__tests__/ReactIncremental-test.js | 49 ++++++++++ 6 files changed, 254 insertions(+), 84 deletions(-) diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index 5461d40515..b629dbd0a1 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -28,7 +28,7 @@ var { var ReactFiber = require('ReactFiber'); var ReactReifiedYield = require('ReactReifiedYield'); -function createSubsequentChild(parent : Fiber, previousSibling : Fiber, newChildren) : Fiber { +function createSubsequentChild(parent : Fiber, nextReusable : ?Fiber, previousSibling : Fiber, newChildren) : Fiber { if (typeof newChildren !== 'object' || newChildren === null) { return previousSibling; } @@ -36,6 +36,18 @@ function createSubsequentChild(parent : Fiber, previousSibling : Fiber, newChild switch (newChildren.$$typeof) { case REACT_ELEMENT_TYPE: { const element = (newChildren : ReactElement); + if (nextReusable && + element.type === nextReusable.type && + element.key === nextReusable.key) { + // TODO: This is not sufficient since previous siblings could be new. + // Will fix reconciliation properly later. + const clone = ReactFiber.cloneFiber(nextReusable); + clone.input = element.props; + clone.child = nextReusable.child; + clone.sibling = null; + previousSibling.sibling = clone; + return clone; + } const child = ReactFiber.createFiberFromElement(element); previousSibling.sibling = child; child.parent = parent; @@ -64,7 +76,11 @@ function createSubsequentChild(parent : Fiber, previousSibling : Fiber, newChild if (Array.isArray(newChildren)) { let prev : Fiber = previousSibling; for (var i = 0; i < newChildren.length; i++) { - prev = createSubsequentChild(parent, prev, newChildren[i]); + let reusable = null; + if (prev.alternate) { + reusable = prev.alternate.sibling; + } + prev = createSubsequentChild(parent, reusable, prev, newChildren[i]); } return prev; } else { @@ -81,6 +97,17 @@ function createFirstChild(parent, newChildren) { switch (newChildren.$$typeof) { case REACT_ELEMENT_TYPE: { const element = (newChildren : ReactElement); + const existingChild : ?Fiber = parent.child; + if (existingChild && + element.type === existingChild.type && + element.key === existingChild.key) { + // Get the clone of the existing fiber. + const clone = ReactFiber.cloneFiber(existingChild); + clone.input = element.props; + clone.child = existingChild.child; + clone.sibling = null; + return clone; + } const child = ReactFiber.createFiberFromElement(element); child.parent = parent; return child; @@ -114,7 +141,11 @@ function createFirstChild(parent, newChildren) { prev = createFirstChild(parent, newChildren[i]); first = prev; } else { - prev = createSubsequentChild(parent, prev, newChildren[i]); + let reusable = null; + if (prev.alternate) { + reusable = prev.alternate.sibling; + } + prev = createSubsequentChild(parent, reusable, prev, newChildren[i]); } } return first; diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index 918ef87dd4..86e5cbaed1 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -25,28 +25,47 @@ var ReactElement = require('ReactElement'); import type { ReactCoroutine, ReactYield } from 'ReactCoroutine'; -export type Fiber = { +// An Instance is shared between all versions of a component. We can easily +// break this out into a separate object to avoid copying so much to the +// alternate versions of the tree. We put this on a single object for now to +// minimize the number of objects created during the initial render. +type Instance = { // Tag identifying the type of fiber. tag: number, - // Singly Linked List Tree Structure. - parent: ?Fiber, // Consider a regenerated temporary parent stack instead. - child: ?Fiber, - sibling: ?Fiber, + // The parent Fiber used to create this one. The type is constrained to the + // Instance part of the Fiber since it is not safe to traverse the tree from + // the instance. + parent: ?Instance, // Consider a regenerated temporary parent stack instead. // Unique identifier of this child. - key: ?string, + key: null | string, // The function/class/module associated with this fiber. type: any, + // The local state associated with this fiber. + stateNode: ?Object, + +}; + +// A Fiber is work on a Component that needs to be done or was done. There can +// be more than one per component. +export type Fiber = Instance & { + + // Singly Linked List Tree Structure. + child: ?Fiber, + sibling: ?Fiber, + // The ref last used to attach this node. // I'll avoid adding an owner field for prod and model that as functions. ref: null | (handle : ?Object) => void, // Input is the data coming into process this fiber. Arguments. Props. input: any, // This type will be more specific once we overload the tag. + // TODO: I think that there is a way to merge input and memoizedInput somehow. + memoizedInput: any, // The input 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. @@ -54,30 +73,42 @@ export type Fiber = { // This will be used to quickly determine if a subtree has no pending changes. hasPendingChanges: bool, - // The local state associated with this fiber. - stateNode: ?Object, + // This is a pooled version of a Fiber. Every fiber that gets updated will + // eventually have a pair. There are cases when we can clean up pairs to save + // memory if we need to. + alternate: ?Fiber, }; var createFiber = function(tag : number, key : null | string) : Fiber { return { + // Instance + tag: tag, parent: null, + + key: key, + + type: null, + + stateNode: null, + + // Fiber + child: null, sibling: null, - key: key, - type: null, ref: null, input: null, + memoizedInput: null, output: null, hasPendingChanges: true, - stateNode: null, + alternate: null, }; }; @@ -86,6 +117,33 @@ function shouldConstruct(Component) { return !!(Component.prototype && Component.prototype.isReactComponent); } +// This is used to create an alternate fiber to do work on. +exports.cloneFiber = function(fiber : Fiber) : Fiber { + // We use a double buffering pooling technique because we know that we'll only + // ever need at most two versions of a tree. We pool the "other" unused node + // that we're free to reuse. This is lazily created to avoid allocating extra + // objects for things that are never updated. It also allow us to reclaim the + // extra memory if needed. + if (fiber.alternate) { + return fiber.alternate; + } + // This should not have an alternate already + var alt = createFiber(fiber.tag, fiber.key); + + if (fiber.parent) { + // TODO: This assumes the parent's alternate is already created. + // Stop using the alternates of parents once we have a parent stack. + // $FlowFixMe: This downcast is not safe. It is intentionally an error. + alt.parent = fiber.parent.alternate; + } + + alt.type = fiber.type; + alt.stateNode = fiber.stateNode; + alt.alternate = fiber; + fiber.alternate = alt; + return alt; +}; + exports.createFiberFromElement = function(element : ReactElement) { const fiber = exports.createFiberFromElementType(element.type, element.key); fiber.input = element.props; diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 91ee35a2f0..1cf9081599 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -27,100 +27,114 @@ var { YieldComponent, } = ReactTypesOfWork; -function updateFunctionalComponent(unitOfWork) { - var fn = unitOfWork.type; - var props = unitOfWork.input; - console.log('perform work on:', fn.name); +function updateFunctionalComponent(workInProgress) { + var fn = workInProgress.type; + var props = workInProgress.input; + console.log('update fn:', fn.name); var nextChildren = fn(props); - unitOfWork.child = ReactChildFiber.reconcileChildFibers( - unitOfWork, - unitOfWork.child, + workInProgress.child = ReactChildFiber.reconcileChildFibers( + workInProgress, + workInProgress.child, nextChildren ); } -function updateHostComponent(unitOfWork) { - console.log('host component', unitOfWork.type, typeof unitOfWork.input.children === 'string' ? unitOfWork.input.children : ''); +function updateHostComponent(workInProgress) { + console.log('host component', workInProgress.type, typeof workInProgress.input.children === 'string' ? workInProgress.input.children : ''); - var nextChildren = unitOfWork.input.children; - unitOfWork.child = ReactChildFiber.reconcileChildFibers( - unitOfWork, - unitOfWork.child, + var nextChildren = workInProgress.input.children; + workInProgress.child = ReactChildFiber.reconcileChildFibers( + workInProgress, + workInProgress.child, nextChildren ); } -function mountIndeterminateComponent(unitOfWork) { - var fn = unitOfWork.type; - var props = unitOfWork.input; +function mountIndeterminateComponent(workInProgress) { + var fn = workInProgress.type; + var props = workInProgress.input; var value = fn(props); if (typeof value === 'object' && value && typeof value.render === 'function') { console.log('performed work on class:', fn.name); // Proceed under the assumption that this is a class instance - unitOfWork.tag = ClassComponent; + workInProgress.tag = ClassComponent; } else { console.log('performed work on fn:', fn.name); // Proceed under the assumption that this is a functional component - unitOfWork.tag = FunctionalComponent; + workInProgress.tag = FunctionalComponent; } - unitOfWork.child = ReactChildFiber.reconcileChildFibers( - unitOfWork, - unitOfWork.child, + workInProgress.child = ReactChildFiber.reconcileChildFibers( + workInProgress, + workInProgress.child, value ); } -function updateCoroutineComponent(unitOfWork) { - var coroutine = (unitOfWork.input : ?ReactCoroutine); +function updateCoroutineComponent(workInProgress) { + var coroutine = (workInProgress.input : ?ReactCoroutine); if (!coroutine) { throw new Error('Should be resolved by now'); } - console.log('begin coroutine', unitOfWork.type.name); - unitOfWork.child = ReactChildFiber.reconcileChildFibers( - unitOfWork, - unitOfWork.child, + console.log('begin coroutine', workInProgress.type.name); + workInProgress.child = ReactChildFiber.reconcileChildFibers( + workInProgress, + workInProgress.child, coroutine.children ); } -function beginWork(unitOfWork : Fiber) : ?Fiber { - switch (unitOfWork.tag) { +function beginWork(workInProgress : Fiber) : ?Fiber { + const alt = workInProgress.alternate; + if (alt && workInProgress.input === alt.memoizedInput) { + // The most likely scenario is that the previous copy of the tree contains + // the same input as the new one. In that case, we can just copy the output + // and children from that node. + workInProgress.output = alt.output; + workInProgress.child = alt.child; + return null; + } + if (workInProgress.input === workInProgress.memoizedInput) { + // In a ping-pong scenario, this version could actually contain the + // old input. In that case, we can just bail out. + return null; + } + switch (workInProgress.tag) { case IndeterminateComponent: - mountIndeterminateComponent(unitOfWork); + mountIndeterminateComponent(workInProgress); break; case FunctionalComponent: - updateFunctionalComponent(unitOfWork); + updateFunctionalComponent(workInProgress); break; case ClassComponent: - console.log('class component', unitOfWork.input.type.name); + console.log('class component', workInProgress.input.type.name); break; case HostComponent: - updateHostComponent(unitOfWork); + updateHostComponent(workInProgress); break; case CoroutineHandlerPhase: // This is a restart. Reset the tag to the initial phase. - unitOfWork.tag = CoroutineComponent; + workInProgress.tag = CoroutineComponent; // Intentionally fall through since this is now the same. case CoroutineComponent: - updateCoroutineComponent(unitOfWork); + updateCoroutineComponent(workInProgress); // This doesn't take arbitrary time so we could synchronously just begin - // eagerly do the work of unitOfWork.child as an optimization. - if (unitOfWork.child) { - return beginWork(unitOfWork.child); + // eagerly do the work of workInProgress.child as an optimization. + if (workInProgress.child) { + return beginWork(workInProgress.child); } break; case YieldComponent: // A yield component is just a placeholder, we can just run through the // next one immediately. - if (unitOfWork.sibling) { - return beginWork(unitOfWork.sibling); + if (workInProgress.sibling) { + return beginWork(workInProgress.sibling); } return null; default: throw new Error('Unknown unit of work tag'); } - return unitOfWork.child; + return workInProgress.child; } exports.beginWork = beginWork; diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index acc757241a..1ec3e0c1e3 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -34,6 +34,7 @@ function transferOutput(child : ?Fiber, parent : Fiber) { // avoid unnecessary traversal. When we have multiple output, we just pass // the linked list of fibers that has the individual output values. parent.output = (child && !child.sibling) ? child.output : child; + parent.memoizedInput = parent.input; } function recursivelyFillYields(yields, output : ?Fiber | ?ReifiedYield) { @@ -53,8 +54,8 @@ function recursivelyFillYields(yields, output : ?Fiber | ?ReifiedYield) { } } -function moveCoroutineToHandlerPhase(unitOfWork : Fiber) { - var coroutine = (unitOfWork.input : ?ReactCoroutine); +function moveCoroutineToHandlerPhase(workInProgress : Fiber) { + var coroutine = (workInProgress.input : ?ReactCoroutine); if (!coroutine) { throw new Error('Should be resolved by now'); } @@ -64,12 +65,14 @@ function moveCoroutineToHandlerPhase(unitOfWork : Fiber) { // single component, or at least tail call optimize nested ones. Currently // that requires additional fields that we don't want to add to the fiber. // So this requires nested handlers. - unitOfWork.tag = CoroutineHandlerPhase; + // Note: This doesn't mutate the alternate node. I don't think it needs to + // since this stage is reset for every pass. + workInProgress.tag = CoroutineHandlerPhase; // Build up the yields. // TODO: Compare this to a generator or opaque helpers like Children. var yields : Array = []; - var child = unitOfWork.child; + var child = workInProgress.child; while (child) { recursivelyFillYields(yields, child.output); child = child.sibling; @@ -78,34 +81,34 @@ function moveCoroutineToHandlerPhase(unitOfWork : Fiber) { var props = coroutine.props; var nextChildren = fn(props, yields); - unitOfWork.stateNode = ReactChildFiber.reconcileChildFibers( - unitOfWork, - unitOfWork.stateNode, + workInProgress.stateNode = ReactChildFiber.reconcileChildFibers( + workInProgress, + workInProgress.stateNode, nextChildren ); - return unitOfWork.stateNode; + return workInProgress.stateNode; } -exports.completeWork = function(unitOfWork : Fiber) : ?Fiber { - switch (unitOfWork.tag) { +exports.completeWork = function(workInProgress : Fiber) : ?Fiber { + switch (workInProgress.tag) { case FunctionalComponent: - console.log('/functional component', unitOfWork.type.name); - transferOutput(unitOfWork.child, unitOfWork); + console.log('/functional component', workInProgress.type.name); + transferOutput(workInProgress.child, workInProgress); break; case ClassComponent: - console.log('/class component', unitOfWork.type.name); - transferOutput(unitOfWork.child, unitOfWork); + console.log('/class component', workInProgress.type.name); + transferOutput(workInProgress.child, workInProgress); break; case HostComponent: - console.log('/host component', unitOfWork.type); + console.log('/host component', workInProgress.type); break; case CoroutineComponent: - console.log('/coroutine component', unitOfWork.input.handler.name); - return moveCoroutineToHandlerPhase(unitOfWork); + console.log('/coroutine component', workInProgress.input.handler.name); + return moveCoroutineToHandlerPhase(workInProgress); case CoroutineHandlerPhase: - transferOutput(unitOfWork.stateNode, unitOfWork); + transferOutput(workInProgress.stateNode, workInProgress); // Reset the tag to now be a first phase coroutine. - unitOfWork.tag = CoroutineComponent; + workInProgress.tag = CoroutineComponent; break; case YieldComponent: // Does nothing. diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index c590e35f3e..18a234d727 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -49,18 +49,23 @@ module.exports = function(config : HostConfig) : Reconciler { let nextUnitOfWork : ?Fiber = null; - function completeUnitOfWork(unitOfWork : Fiber) : ?Fiber { + function completeUnitOfWork(workInProgress : Fiber) : ?Fiber { while (true) { - var next = completeWork(unitOfWork); + var next = completeWork(workInProgress); if (next) { // If completing this work spawned new work, do that next. return next; - } else if (unitOfWork.sibling) { + } else if (workInProgress.sibling) { // If there is more work to do in this parent, do that next. - return unitOfWork.sibling; - } else if (unitOfWork.parent) { + return workInProgress.sibling; + } else if (workInProgress.parent) { // If there's no more work in this parent. Complete the parent. - unitOfWork = unitOfWork.parent; + // TODO: Stop using the parent for this purpose. I think this will break + // down in edge cases because when nodes are reused during bailouts, we + // don't know which of two parents was used. Instead we should maintain + // a temporary manual stack. + // $FlowFixMe: This downcast is not safe. It is intentionally an error. + workInProgress = workInProgress.parent; } else { // If we're at the root, there's no more work to do. return null; @@ -68,14 +73,14 @@ module.exports = function(config : HostConfig) : Reconciler { } } - function performUnitOfWork(unitOfWork : Fiber) : ?Fiber { - var next = beginWork(unitOfWork); + function performUnitOfWork(workInProgress : Fiber) : ?Fiber { + var next = beginWork(workInProgress); if (next) { // If this spawns new work, do that next. return next; } else { // Otherwise, complete the current work. - return completeUnitOfWork(unitOfWork); + return completeUnitOfWork(workInProgress); } } @@ -107,13 +112,23 @@ module.exports = function(config : HostConfig) : Reconciler { } */ + let rootFiber : ?Fiber = null; + return { mountNewRoot(element : ReactElement) : OpaqueID { ensureLowPriIsScheduled(); - nextUnitOfWork = ReactFiber.createFiberFromElement(element); + // TODO: Unify this with ReactChildFiber. We can't now because the parent + // is passed. Should be doable though. Might require a wrapper don't know. + if (rootFiber && rootFiber.type === element.type && rootFiber.key === element.key) { + nextUnitOfWork = rootFiber; + rootFiber.input = element.props; + return {}; + } + + nextUnitOfWork = rootFiber = ReactFiber.createFiberFromElement(element); return {}; }, diff --git a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js index c0bc0abd32..5d1af26bcf 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js @@ -66,4 +66,53 @@ describe('ReactIncremental', function() { expect(barCalled).toBe(true); }); + it('updates a previous render', function() { + + var ops = []; + + function Header() { + ops.push('Header'); + return

Hi

; + } + + function Content(props) { + ops.push('Content'); + return
{props.children}
; + } + + function Footer() { + ops.push('Footer'); + return
Bye
; + } + + var header =
; + var footer =