From cce58ffd621cc200bbeb52af55dc0b2b0ed87b76 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 6 Jun 2016 22:00:51 -0700 Subject: [PATCH] Simple updates using alternate fibers This splits the Fiber type into Fiber and Instance. This could be two different object instances to save memory. However, to avoid GC thrash I merge them into one. When ReactChildFiber reconciles children, it clones the previous fiber. This creates a new tree for work-in-progress. The idea is that once flushed, this new tree will be used at the root. However, we know that we'll never need more than two trees at a time. Therefore my clone function stores the clone on the original. Effectively this creates a fiber pool. Ideally, the .alternate field shouldn't be used outside of clone so that everything can work with pure immutability. I cheat a bit for now so I don't have to pass both trees everywhere. ReactChildFiber is a bit hacky for reuse and doesn't solve all cases. Will fix that once I try to get parity. --- src/renderers/shared/fiber/ReactChildFiber.js | 37 ++++++++- src/renderers/shared/fiber/ReactFiber.js | 80 ++++++++++++++++--- .../shared/fiber/ReactFiberBeginWork.js | 16 +++- .../shared/fiber/ReactFiberCompleteWork.js | 3 + .../shared/fiber/ReactFiberReconciler.js | 17 +++- .../fiber/__tests__/ReactIncremental-test.js | 49 ++++++++++++ 6 files changed, 186 insertions(+), 16 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..60b90d472d 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -30,7 +30,7 @@ var { function updateFunctionalComponent(unitOfWork) { var fn = unitOfWork.type; var props = unitOfWork.input; - console.log('perform work on:', fn.name); + console.log('update fn:', fn.name); var nextChildren = fn(props); unitOfWork.child = ReactChildFiber.reconcileChildFibers( @@ -85,6 +85,20 @@ function updateCoroutineComponent(unitOfWork) { } function beginWork(unitOfWork : Fiber) : ?Fiber { + const alt = unitOfWork.alternate; + if (alt && unitOfWork.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. + unitOfWork.output = alt.output; + unitOfWork.child = alt.child; + return null; + } + if (unitOfWork.input === unitOfWork.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 (unitOfWork.tag) { case IndeterminateComponent: mountIndeterminateComponent(unitOfWork); diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index acc757241a..9277bf63ff 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) { @@ -64,6 +65,8 @@ 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. + // Note: This doesn't mutate the alternate node. I don't think it needs to + // since this stage is reset for every pass. unitOfWork.tag = CoroutineHandlerPhase; // Build up the yields. diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index c590e35f3e..18165b257a 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -60,6 +60,11 @@ module.exports = function(config : HostConfig) : Reconciler { return unitOfWork.sibling; } else if (unitOfWork.parent) { // If there's no more work in this parent. Complete the 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. unitOfWork = unitOfWork.parent; } else { // If we're at the root, there's no more work to do. @@ -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 =