@samitouri / QOS-React-2 / commits / e0a305350b

Add Fragment fiber type

We currently treat nested arrays as a unique namespace from top level children. So that these won't share key namespaces. This adds a new fiber type that will represent the position of a fragment. This is only used for nested arrays. Even if you return an array from a composite component, we don't need this since they share namespace with a single top level component. This still doesn't implement the complete reconciliation algorthim in child fiber. That's coming later.

Sebastian Markbage committed Sep 13, 2016 at 10:56 UTC e0a305350b16f02857c1d6111f7a1a5c97e4bf25
8 files changed +200 -45
src/renderers/shared/fiber/ReactChildFiber.js
+36 -40
@@ -30,6 +30,7 @@ var ReactReifiedYield = require('ReactReifiedYield');
30 const {
31 cloneFiber,
32 createFiberFromElement,
33 + createFiberFromFragment,
34 createFiberFromCoroutine,
35 createFiberFromYield,
36 } = ReactFiber;
@@ -46,7 +47,7 @@ function ChildReconciler(shouldClone) {
47 returnFiber : Fiber,
48 existingChild : ?Fiber,
49 previousSibling : Fiber,
49 - newChildren,
50 + newChildren : any,
51 priority : PriorityLevel
52 ) : Fiber {
53 if (typeof newChildren !== 'object' || newChildren === null) {
@@ -98,35 +99,24 @@ function ChildReconciler(shouldClone) {
99 }
100
101 if (isArray(newChildren)) {
101 - let prev : Fiber = previousSibling;
102 - let existing : ?Fiber = existingChild;
103 - for (var i = 0; i < newChildren.length; i++) {
104 - var nextExisting = existing && existing.sibling;
105 - prev = createSubsequentChild(returnFiber, existing, prev, newChildren[i], priority);
106 - if (prev && existing) {
107 - // TODO: This is not correct because there could've been more
108 - // than one sibling consumed but I don't want to return a tuple.
109 - existing = nextExisting;
110 - }
111 - }
112 - return prev;
102 + // Nested arrays have a special fiber type.
103 + const fragment = createFiberFromFragment(newChildren, priority);
104 + previousSibling.sibling = fragment;
105 + fragment.return = returnFiber;
106 + return fragment;
107 } else {
108 // TODO: Throw for unknown children.
109 return previousSibling;
110 }
111 }
112
119 - function createFirstChild(returnFiber, existingChild, newChildren, priority) {
113 + function createFirstChild(returnFiber, existingChild, newChildren : any, priority) {
114 if (typeof newChildren !== 'object' || newChildren === null) {
115 return null;
116 }
117
118 switch (newChildren.$$typeof) {
119 case REACT_ELEMENT_TYPE: {
126 - /* $FlowFixMe(>=0.31.0): This is an unsafe cast. Consider adding a type
127 - * annotation to the `newChildren` param of this
128 - * function.
129 - */
120 const element = (newChildren : ReactElement<any>);
121 if (existingChild &&
122 element.type === existingChild.type &&
@@ -148,8 +138,6 @@ function ChildReconciler(shouldClone) {
138 }
139
140 case REACT_COROUTINE_TYPE: {
151 - /* $FlowFixMe(>=0.31.0): No 'handler' property found in object type
152 - */
141 const coroutine = (newChildren : ReactCoroutine);
142 const child = createFiberFromCoroutine(coroutine, priority);
143 child.return = returnFiber;
@@ -160,9 +148,6 @@ function ChildReconciler(shouldClone) {
148 // A yield results in a fragment fiber whose output is the continuation.
149 // TODO: When there is only a single child, we can optimize this to avoid
150 // the fragment.
163 - /* $FlowFixMe(>=0.31.0): No 'continuation' property found in object
164 - * type
165 - */
151 const yieldNode = (newChildren : ReactYield);
152 const reifiedYield = createReifiedYield(yieldNode);
153 const child = createFiberFromYield(yieldNode, priority);
@@ -173,14 +158,38 @@ function ChildReconciler(shouldClone) {
158 }
159
160 if (isArray(newChildren)) {
176 - var first : ?Fiber = null;
177 - var prev : ?Fiber = null;
178 - var existing : ?Fiber = existingChild;
161 + // Nested arrays have a special fiber type.
162 + const fragment = createFiberFromFragment(newChildren, priority);
163 + fragment.return = returnFiber;
164 + return fragment;
165 + } else {
166 + // TODO: Throw for unknown children.
167 + return null;
168 + }
169 + }
170 +
171 + // TODO: This API won't work because we'll need to transfer the side-effects of
172 + // unmounting children to the returnFiber.
173 + function reconcileChildFibers(
174 + returnFiber : Fiber,
175 + currentFirstChild : ?Fiber,
176 + newChild : ReactNodeList,
177 + priority : PriorityLevel
178 + ) : ?Fiber {
179 + // This function is not recursive.
180 + // If the top level item is an array, we treat it as a set of children,
181 + // not as a fragment. Nested arrays on the other hand will be treated as
182 + // fragment nodes. Recursion happens at the normal flow.
183 + if (isArray(newChild)) {
184 /* $FlowIssue(>=0.31.0) #12747709
185 *
186 * `Array.isArray` is matched syntactically for now until predicate
187 * support is complete.
188 */
189 + var newChildren : Array<*> = newChild;
190 + var first : ?Fiber = null;
191 + var prev : ?Fiber = null;
192 + var existing : ?Fiber = currentFirstChild;
193 for (var i = 0; i < newChildren.length; i++) {
194 var nextExisting = existing && existing.sibling;
195 if (prev == null) {
@@ -196,21 +205,8 @@ function ChildReconciler(shouldClone) {
205 }
206 }
207 return first;
199 - } else {
200 - // TODO: Throw for unknown children.
201 - return null;
208 }
203 - }
204 -
205 - // TODO: This API won't work because we'll need to transfer the side-effects of
206 - // unmounting children to the returnFiber.
207 - function reconcileChildFibers(
208 - returnFiber : Fiber,
209 - currentFirstChild : ?Fiber,
210 - newChildren : ReactNodeList,
211 - priority : PriorityLevel
212 - ) : ?Fiber {
213 - return createFirstChild(returnFiber, currentFirstChild, newChildren, priority);
209 + return createFirstChild(returnFiber, currentFirstChild, newChild, priority);
210 }
211
212 return reconcileChildFibers;
src/renderers/shared/fiber/ReactFiber.js
+12
@@ -12,6 +12,7 @@
12
13 'use strict';
14
15 +import type { ReactFragment } from 'ReactTypes';
16 import type { ReactCoroutine, ReactYield } from 'ReactCoroutine';
17 import type { TypeOfWork } from 'ReactTypeOfWork';
18 import type { PriorityLevel } from 'ReactPriorityLevel';
@@ -25,6 +26,7 @@ var {
26 HostComponent,
27 CoroutineComponent,
28 YieldComponent,
29 + Fragment,
30 } = ReactTypeOfWork;
31
32 var {
@@ -245,6 +247,15 @@ exports.createFiberFromElement = function(element : ReactElement<*>, priorityLev
247 return fiber;
248 };
249
250 +exports.createFiberFromFragment = function(elements : ReactFragment, priorityLevel : PriorityLevel) {
251 + // TODO: Consider supporting keyed fragments. Technically, we accidentally
252 + // support that in the existing React.
253 + const fiber = createFiber(Fragment, null);
254 + fiber.pendingProps = elements;
255 + fiber.pendingWorkPriority = priorityLevel;
256 + return fiber;
257 +};
258 +
259 function createFiberFromElementType(type : mixed, key : null | string) {
260 let fiber;
261 if (typeof type === 'function') {
@@ -279,3 +290,4 @@ exports.createFiberFromYield = function(yieldNode : ReactYield, priorityLevel :
290 fiber.pendingProps = {};
291 return fiber;
292 };
293 +
src/renderers/shared/fiber/ReactFiberBeginWork.js
+16
@@ -36,6 +36,7 @@ var {
36 CoroutineComponent,
37 CoroutineHandlerPhase,
38 YieldComponent,
39 + Fragment,
40 } = ReactTypeOfWork;
41 var {
42 NoWork,
@@ -97,6 +98,11 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getSchedu
98 markChildAsProgressed(current, workInProgress, priorityLevel);
99 }
100
101 + function updateFragment(current, workInProgress) {
102 + var nextChildren = workInProgress.pendingProps;
103 + reconcileChildren(current, workInProgress, nextChildren);
104 + }
105 +
106 function updateFunctionalComponent(current, workInProgress) {
107 var fn = workInProgress.type;
108 var props = workInProgress.pendingProps;
@@ -412,6 +418,16 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getSchedu
418 );
419 }
420 return null;
421 + case Fragment:
422 + updateFragment(current, workInProgress);
423 + if (workInProgress.child) {
424 + return beginWork(
425 + workInProgress.child.alternate,
426 + workInProgress.child,
427 + priorityLevel
428 + );
429 + }
430 + return null;
431 default:
432 throw new Error('Unknown unit of work tag');
433 }
src/renderers/shared/fiber/ReactFiberCompleteWork.js
+4
@@ -28,6 +28,7 @@ var {
28 CoroutineComponent,
29 CoroutineHandlerPhase,
30 YieldComponent,
31 + Fragment,
32 } = ReactTypeOfWork;
33
34 module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
@@ -199,6 +200,9 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
200 case YieldComponent:
201 // Does nothing.
202 return null;
203 + case Fragment:
204 + transferOutput(workInProgress.child, workInProgress);
205 + return null;
206
207 // Error cases
208 case IndeterminateComponent:
src/renderers/shared/fiber/ReactTypeOfWork.js
+2 -1
@@ -12,7 +12,7 @@
12
13 'use strict';
14
15 -export type TypeOfWork = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
15 +export type TypeOfWork = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
16
17 module.exports = {
18 IndeterminateComponent: 0, // Before we know whether it is functional or class
@@ -23,4 +23,5 @@ module.exports = {
23 CoroutineComponent: 5,
24 CoroutineHandlerPhase: 6,
25 YieldComponent: 7,
26 + Fragment: 8,
27 };
src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js
+32
@@ -57,6 +57,38 @@ describe('ReactIncrementalSideEffects', () => {
57
58 });
59
60 + it('can update child nodes of a fragment', function() {
61 +
62 + function Bar(props) {
63 + return <span>{props.text}</span>;
64 + }
65 +
66 + function Foo(props) {
67 + return (
68 + <div>
69 + <Bar text={props.text} />
70 + {props.text === 'World' ? [
71 + <Bar key="a" text={props.text} />,
72 + <div key="b" />,
73 + ] : null}
74 + </div>
75 + );
76 + }
77 +
78 + ReactNoop.render(<Foo text="Hello" />);
79 + ReactNoop.flush();
80 + expect(ReactNoop.root.children).toEqual([
81 + div(span()),
82 + ]);
83 +
84 + ReactNoop.render(<Foo text="World" />);
85 + ReactNoop.flush();
86 + expect(ReactNoop.root.children).toEqual([
87 + div(span(), span(), div()),
88 + ]);
89 +
90 + });
91 +
92 it('does not update child nodes if a flush is aborted', () => {
93
94 function Bar(props) {
src/renderers/shared/fiber/__tests__/ReactTopLevelFragment-test.js new
+97
@@ -0,0 +1,97 @@
1 +/**
2 + * Copyright 2013-present, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @emails react-core
10 + */
11 +
12 +'use strict';
13 +
14 +var React;
15 +var ReactNoop;
16 +
17 +// This is a new feature in Fiber so I put it in its own test file. It could
18 +// probably move to one of the other test files once it is official.
19 +describe('ReactTopLevelFragment', function() {
20 + beforeEach(function() {
21 + React = require('React');
22 + ReactNoop = require('ReactNoop');
23 + });
24 +
25 + it('should render a simple fragment at the top of a component', function() {
26 +
27 + function Fragment() {
28 + return [<div key="a">Hello</div>, <div key="b">World</div>];
29 + }
30 + ReactNoop.render(<Fragment />);
31 + ReactNoop.flush();
32 +
33 + });
34 +
35 + it('should preserve state when switching from a single child', function() {
36 +
37 + var instance = null;
38 +
39 + class Stateful extends React.Component {
40 + render() {
41 + instance = this;
42 + return <div>Hello</div>;
43 + }
44 + }
45 +
46 + function Fragment({ condition }) {
47 + return condition ? <Stateful key="a" /> :
48 + [<Stateful key="a" />, <div key="b">World</div>];
49 + }
50 + ReactNoop.render(<Fragment />);
51 + ReactNoop.flush();
52 +
53 + var instanceA = instance;
54 +
55 + expect(instanceA).not.toBe(null);
56 +
57 + ReactNoop.render(<Fragment condition={true} />);
58 + ReactNoop.flush();
59 +
60 + var instanceB = instance;
61 +
62 + expect(instanceB).toBe(instanceA);
63 +
64 + });
65 +
66 + it('should not preserve state when switching to a nested array', function() {
67 +
68 + var instance = null;
69 +
70 + class Stateful extends React.Component {
71 + render() {
72 + instance = this;
73 + return <div>Hello</div>;
74 + }
75 + }
76 +
77 + function Fragment({ condition }) {
78 + return condition ? <Stateful key="a" /> :
79 + [[<Stateful key="a" />, <div key="b">World</div>], <div />];
80 + }
81 + ReactNoop.render(<Fragment />);
82 + ReactNoop.flush();
83 +
84 + var instanceA = instance;
85 +
86 + expect(instanceA).not.toBe(null);
87 +
88 + ReactNoop.render(<Fragment condition={true} />);
89 + ReactNoop.flush();
90 +
91 + var instanceB = instance;
92 +
93 + expect(instanceB).not.toBe(instanceA);
94 +
95 + });
96 +
97 +});
src/renderers/shared/fiber/isomorphic/ReactCoroutine.js
+1 -4
@@ -33,10 +33,7 @@ export type ReactCoroutine = {
33 children: any,
34 // This should be a more specific CoroutineHandler
35 handler: (props: any, yields: Array<ReifiedYield>) => ReactNodeList,
36 - /* $FlowFixMe(>=0.31.0): Which is it? mixed? Or Object? Must match
37 - * `ReactYield` type.
38 - */
39 - props: mixed,
36 + props: any,
37 };
38 export type ReactYield = {
39 $$typeof: Symbol | number,