@samitouri / QOS-React / commits / cce58ffd62

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.

Sebastian Markbage committed Jun 6, 2016 at 22:00 UTC cce58ffd621cc200bbeb52af55dc0b2b0ed87b76
6 files changed +186 -16
src/renderers/shared/fiber/ReactChildFiber.js
+34 -3
@@ -28,7 +28,7 @@ var {
28 var ReactFiber = require('ReactFiber');
29 var ReactReifiedYield = require('ReactReifiedYield');
30
31 -function createSubsequentChild(parent : Fiber, previousSibling : Fiber, newChildren) : Fiber {
31 +function createSubsequentChild(parent : Fiber, nextReusable : ?Fiber, previousSibling : Fiber, newChildren) : Fiber {
32 if (typeof newChildren !== 'object' || newChildren === null) {
33 return previousSibling;
34 }
@@ -36,6 +36,18 @@ function createSubsequentChild(parent : Fiber, previousSibling : Fiber, newChild
36 switch (newChildren.$$typeof) {
37 case REACT_ELEMENT_TYPE: {
38 const element = (newChildren : ReactElement<any>);
39 + if (nextReusable &&
40 + element.type === nextReusable.type &&
41 + element.key === nextReusable.key) {
42 + // TODO: This is not sufficient since previous siblings could be new.
43 + // Will fix reconciliation properly later.
44 + const clone = ReactFiber.cloneFiber(nextReusable);
45 + clone.input = element.props;
46 + clone.child = nextReusable.child;
47 + clone.sibling = null;
48 + previousSibling.sibling = clone;
49 + return clone;
50 + }
51 const child = ReactFiber.createFiberFromElement(element);
52 previousSibling.sibling = child;
53 child.parent = parent;
@@ -64,7 +76,11 @@ function createSubsequentChild(parent : Fiber, previousSibling : Fiber, newChild
76 if (Array.isArray(newChildren)) {
77 let prev : Fiber = previousSibling;
78 for (var i = 0; i < newChildren.length; i++) {
67 - prev = createSubsequentChild(parent, prev, newChildren[i]);
79 + let reusable = null;
80 + if (prev.alternate) {
81 + reusable = prev.alternate.sibling;
82 + }
83 + prev = createSubsequentChild(parent, reusable, prev, newChildren[i]);
84 }
85 return prev;
86 } else {
@@ -81,6 +97,17 @@ function createFirstChild(parent, newChildren) {
97 switch (newChildren.$$typeof) {
98 case REACT_ELEMENT_TYPE: {
99 const element = (newChildren : ReactElement<any>);
100 + const existingChild : ?Fiber = parent.child;
101 + if (existingChild &&
102 + element.type === existingChild.type &&
103 + element.key === existingChild.key) {
104 + // Get the clone of the existing fiber.
105 + const clone = ReactFiber.cloneFiber(existingChild);
106 + clone.input = element.props;
107 + clone.child = existingChild.child;
108 + clone.sibling = null;
109 + return clone;
110 + }
111 const child = ReactFiber.createFiberFromElement(element);
112 child.parent = parent;
113 return child;
@@ -114,7 +141,11 @@ function createFirstChild(parent, newChildren) {
141 prev = createFirstChild(parent, newChildren[i]);
142 first = prev;
143 } else {
117 - prev = createSubsequentChild(parent, prev, newChildren[i]);
144 + let reusable = null;
145 + if (prev.alternate) {
146 + reusable = prev.alternate.sibling;
147 + }
148 + prev = createSubsequentChild(parent, reusable, prev, newChildren[i]);
149 }
150 }
151 return first;
src/renderers/shared/fiber/ReactFiber.js
+69 -11
@@ -25,28 +25,47 @@ var ReactElement = require('ReactElement');
25
26 import type { ReactCoroutine, ReactYield } from 'ReactCoroutine';
27
28 -export type Fiber = {
28 +// An Instance is shared between all versions of a component. We can easily
29 +// break this out into a separate object to avoid copying so much to the
30 +// alternate versions of the tree. We put this on a single object for now to
31 +// minimize the number of objects created during the initial render.
32 +type Instance = {
33
34 // Tag identifying the type of fiber.
35 tag: number,
36
33 - // Singly Linked List Tree Structure.
34 - parent: ?Fiber, // Consider a regenerated temporary parent stack instead.
35 - child: ?Fiber,
36 - sibling: ?Fiber,
37 + // The parent Fiber used to create this one. The type is constrained to the
38 + // Instance part of the Fiber since it is not safe to traverse the tree from
39 + // the instance.
40 + parent: ?Instance, // Consider a regenerated temporary parent stack instead.
41
42 // Unique identifier of this child.
39 - key: ?string,
43 + key: null | string,
44
45 // The function/class/module associated with this fiber.
46 type: any,
47
48 + // The local state associated with this fiber.
49 + stateNode: ?Object,
50 +
51 +};
52 +
53 +// A Fiber is work on a Component that needs to be done or was done. There can
54 +// be more than one per component.
55 +export type Fiber = Instance & {
56 +
57 + // Singly Linked List Tree Structure.
58 + child: ?Fiber,
59 + sibling: ?Fiber,
60 +
61 // The ref last used to attach this node.
62 // I'll avoid adding an owner field for prod and model that as functions.
63 ref: null | (handle : ?Object) => void,
64
65 // Input is the data coming into process this fiber. Arguments. Props.
66 input: any, // This type will be more specific once we overload the tag.
67 + // TODO: I think that there is a way to merge input and memoizedInput somehow.
68 + memoizedInput: any, // The input used to create the output.
69 // Output is the return value of this fiber, or a linked list of return values
70 // if this returns multiple values. Such as a fragment.
71 output: any, // This type will be more specific once we overload the tag.
@@ -54,30 +73,42 @@ export type Fiber = {
73 // This will be used to quickly determine if a subtree has no pending changes.
74 hasPendingChanges: bool,
75
57 - // The local state associated with this fiber.
58 - stateNode: ?Object,
76 + // This is a pooled version of a Fiber. Every fiber that gets updated will
77 + // eventually have a pair. There are cases when we can clean up pairs to save
78 + // memory if we need to.
79 + alternate: ?Fiber,
80
81 };
82
83 var createFiber = function(tag : number, key : null | string) : Fiber {
84 return {
85
86 + // Instance
87 +
88 tag: tag,
89
90 parent: null,
68 - child: null,
69 - sibling: null,
91
92 key: key,
93 +
94 type: null,
95 +
96 + stateNode: null,
97 +
98 + // Fiber
99 +
100 + child: null,
101 + sibling: null,
102 +
103 ref: null,
104
105 input: null,
106 + memoizedInput: null,
107 output: null,
108
109 hasPendingChanges: true,
110
80 - stateNode: null,
111 + alternate: null,
112
113 };
114 };
@@ -86,6 +117,33 @@ function shouldConstruct(Component) {
117 return !!(Component.prototype && Component.prototype.isReactComponent);
118 }
119
120 +// This is used to create an alternate fiber to do work on.
121 +exports.cloneFiber = function(fiber : Fiber) : Fiber {
122 + // We use a double buffering pooling technique because we know that we'll only
123 + // ever need at most two versions of a tree. We pool the "other" unused node
124 + // that we're free to reuse. This is lazily created to avoid allocating extra
125 + // objects for things that are never updated. It also allow us to reclaim the
126 + // extra memory if needed.
127 + if (fiber.alternate) {
128 + return fiber.alternate;
129 + }
130 + // This should not have an alternate already
131 + var alt = createFiber(fiber.tag, fiber.key);
132 +
133 + if (fiber.parent) {
134 + // TODO: This assumes the parent's alternate is already created.
135 + // Stop using the alternates of parents once we have a parent stack.
136 + // $FlowFixMe: This downcast is not safe. It is intentionally an error.
137 + alt.parent = fiber.parent.alternate;
138 + }
139 +
140 + alt.type = fiber.type;
141 + alt.stateNode = fiber.stateNode;
142 + alt.alternate = fiber;
143 + fiber.alternate = alt;
144 + return alt;
145 +};
146 +
147 exports.createFiberFromElement = function(element : ReactElement) {
148 const fiber = exports.createFiberFromElementType(element.type, element.key);
149 fiber.input = element.props;
src/renderers/shared/fiber/ReactFiberBeginWork.js
+15 -1
@@ -30,7 +30,7 @@ var {
30 function updateFunctionalComponent(unitOfWork) {
31 var fn = unitOfWork.type;
32 var props = unitOfWork.input;
33 - console.log('perform work on:', fn.name);
33 + console.log('update fn:', fn.name);
34 var nextChildren = fn(props);
35
36 unitOfWork.child = ReactChildFiber.reconcileChildFibers(
@@ -85,6 +85,20 @@ function updateCoroutineComponent(unitOfWork) {
85 }
86
87 function beginWork(unitOfWork : Fiber) : ?Fiber {
88 + const alt = unitOfWork.alternate;
89 + if (alt && unitOfWork.input === alt.memoizedInput) {
90 + // The most likely scenario is that the previous copy of the tree contains
91 + // the same input as the new one. In that case, we can just copy the output
92 + // and children from that node.
93 + unitOfWork.output = alt.output;
94 + unitOfWork.child = alt.child;
95 + return null;
96 + }
97 + if (unitOfWork.input === unitOfWork.memoizedInput) {
98 + // In a ping-pong scenario, this version could actually contain the
99 + // old input. In that case, we can just bail out.
100 + return null;
101 + }
102 switch (unitOfWork.tag) {
103 case IndeterminateComponent:
104 mountIndeterminateComponent(unitOfWork);
src/renderers/shared/fiber/ReactFiberCompleteWork.js
+3
@@ -34,6 +34,7 @@ function transferOutput(child : ?Fiber, parent : Fiber) {
34 // avoid unnecessary traversal. When we have multiple output, we just pass
35 // the linked list of fibers that has the individual output values.
36 parent.output = (child && !child.sibling) ? child.output : child;
37 + parent.memoizedInput = parent.input;
38 }
39
40 function recursivelyFillYields(yields, output : ?Fiber | ?ReifiedYield) {
@@ -64,6 +65,8 @@ function moveCoroutineToHandlerPhase(unitOfWork : Fiber) {
65 // single component, or at least tail call optimize nested ones. Currently
66 // that requires additional fields that we don't want to add to the fiber.
67 // So this requires nested handlers.
68 + // Note: This doesn't mutate the alternate node. I don't think it needs to
69 + // since this stage is reset for every pass.
70 unitOfWork.tag = CoroutineHandlerPhase;
71
72 // Build up the yields.
src/renderers/shared/fiber/ReactFiberReconciler.js
+16 -1
@@ -60,6 +60,11 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) : Reconciler {
60 return unitOfWork.sibling;
61 } else if (unitOfWork.parent) {
62 // If there's no more work in this parent. Complete the parent.
63 + // TODO: Stop using the parent for this purpose. I think this will break
64 + // down in edge cases because when nodes are reused during bailouts, we
65 + // don't know which of two parents was used. Instead we should maintain
66 + // a temporary manual stack.
67 + // $FlowFixMe: This downcast is not safe. It is intentionally an error.
68 unitOfWork = unitOfWork.parent;
69 } else {
70 // If we're at the root, there's no more work to do.
@@ -107,13 +112,23 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) : Reconciler {
112 }
113 */
114
115 + let rootFiber : ?Fiber = null;
116 +
117 return {
118
119 mountNewRoot(element : ReactElement<any>) : OpaqueID {
120
121 ensureLowPriIsScheduled();
122
116 - nextUnitOfWork = ReactFiber.createFiberFromElement(element);
123 + // TODO: Unify this with ReactChildFiber. We can't now because the parent
124 + // is passed. Should be doable though. Might require a wrapper don't know.
125 + if (rootFiber && rootFiber.type === element.type && rootFiber.key === element.key) {
126 + nextUnitOfWork = rootFiber;
127 + rootFiber.input = element.props;
128 + return {};
129 + }
130 +
131 + nextUnitOfWork = rootFiber = ReactFiber.createFiberFromElement(element);
132
133 return {};
134 },
src/renderers/shared/fiber/__tests__/ReactIncremental-test.js
+49
@@ -66,4 +66,53 @@ describe('ReactIncremental', function() {
66 expect(barCalled).toBe(true);
67 });
68
69 + it('updates a previous render', function() {
70 +
71 + var ops = [];
72 +
73 + function Header() {
74 + ops.push('Header');
75 + return <h1>Hi</h1>;
76 + }
77 +
78 + function Content(props) {
79 + ops.push('Content');
80 + return <div>{props.children}</div>;
81 + }
82 +
83 + function Footer() {
84 + ops.push('Footer');
85 + return <footer>Bye</footer>;
86 + }
87 +
88 + var header = <Header />;
89 + var footer = <Footer />;
90 +
91 + function Foo(props) {
92 + ops.push('Foo');
93 + return (
94 + <div>
95 + {header}
96 + <Content>{props.text}</Content>
97 + {footer}
98 + </div>
99 + );
100 + }
101 +
102 + ReactNoop.render(<Foo text="foo" />);
103 + ReactNoop.flush();
104 +
105 + expect(ops).toEqual(['Foo', 'Header', 'Content', 'Footer']);
106 +
107 + ops = [];
108 +
109 + ReactNoop.render(<Foo text="bar" />);
110 + ReactNoop.flush();
111 +
112 + // Since this is an update, it should bail out and reuse the work from
113 + // Header and Content.
114 + expect(ops).toEqual(['Foo', 'Content']);
115 +
116 + });
117 +
118 });