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

Add Text node types

These nodes rendering into Text nodes in the DOM. There is a special case when a string is a direct child of a host node. In that case, we won't reconcile it as a child fiber. In terms of fibers, it is terminal. However, the host config special cases it. It is kind of unfortunate that we have to special case this kind of child in the HostConfig. It would be nice to unify this with other types of child instances. Text nodes have some weird special cases, but those special cases tend to *vary* by environment. They're not the same special cases so not sure how valuable it is to have a special protocol and special types for it.

Sebastian Markbage committed Sep 13, 2016 at 18:36 UTC cafa7b284a7f1830a443525ec08ae26e14ba7cc6
11 files changed +169 -37
src/renderers/dom/fiber/ReactDOMFiber.js
+20 -9
@@ -23,13 +23,14 @@ type DOMContainerElement = Element & { _reactRootContainer: ?Object };
23 type Container = Element;
24 type Props = { };
25 type Instance = Element;
26 +type TextInstance = Text;
27
27 -function recursivelyAppendChildren(parent : Element, child : HostChildren<Instance>) {
28 +function recursivelyAppendChildren(parent : Element, child : HostChildren<Instance | TextInstance>) {
29 if (!child) {
30 return;
31 }
31 - /* $FlowFixMe: Element should have this property. */
32 - if (child.nodeType === 1) {
32 + /* $FlowFixMe: Element and Text should have this property. */
33 + if (child.nodeType === 1 || child.nodeType === 3) {
34 /* $FlowFixMe: Refinement issue. I don't know how to express different. */
35 parent.appendChild(child);
36 } else {
@@ -43,15 +44,16 @@ function recursivelyAppendChildren(parent : Element, child : HostChildren<Instan
44
45 var DOMRenderer = ReactFiberReconciler({
46
46 - updateContainer(container : Container, children : HostChildren<Instance>) : void {
47 + updateContainer(container : Container, children : HostChildren<Instance | TextInstance>) : void {
48 container.innerHTML = '';
49 recursivelyAppendChildren(container, children);
50 },
51
51 - createInstance(type : string, props : Props, children : HostChildren<Instance>) : Instance {
52 + createInstance(type : string, props : Props, children : HostChildren<Instance | TextInstance>) : Instance {
53 const domElement = document.createElement(type);
54 recursivelyAppendChildren(domElement, children);
54 - if (typeof props.children === 'string') {
55 + if (typeof props.children === 'string' ||
56 + typeof props.children === 'number') {
57 domElement.textContent = props.children;
58 }
59 return domElement;
@@ -61,15 +63,16 @@ var DOMRenderer = ReactFiberReconciler({
63 domElement : Instance,
64 oldProps : Props,
65 newProps : Props,
64 - children : HostChildren<Instance>
66 + children : HostChildren<Instance | TextInstance>
67 ) : boolean {
68 return true;
69 },
70
69 - commitUpdate(domElement : Instance, oldProps : Props, newProps : Props, children : HostChildren<Instance>) : void {
71 + commitUpdate(domElement : Instance, oldProps : Props, newProps : Props, children : HostChildren<Instance | TextInstance>) : void {
72 domElement.innerHTML = '';
73 recursivelyAppendChildren(domElement, children);
72 - if (typeof newProps.children === 'string') {
74 + if (typeof newProps.children === 'string' ||
75 + typeof newProps.children === 'number') {
76 domElement.textContent = newProps.children;
77 }
78 },
@@ -78,6 +81,14 @@ var DOMRenderer = ReactFiberReconciler({
81 // Noop
82 },
83
84 + createTextInstance(text : string) : TextInstance {
85 + return document.createTextNode(text);
86 + },
87 +
88 + commitTextUpdate(textInstance : TextInstance, oldText : string, newText : string) : void {
89 + textInstance.nodeValue = newText;
90 + },
91 +
92 scheduleAnimationCallback: window.requestAnimationFrame,
93
94 scheduleDeferredCallback: window.requestIdleCallback,
src/renderers/noop/ReactNoop.js
+25 -12
@@ -33,17 +33,18 @@ var scheduledDeferredCallback = null;
33
34 const TERMINAL_TAG = 99;
35
36 -type Container = { rootID: number, children: Array<Instance> };
36 +type Container = { rootID: number, children: Array<Instance | TextInstance> };
37 type Props = { prop: any };
38 -type Instance = { tag: 99, type: string, id: number, children: Array<Instance>, prop: any };
38 +type Instance = { tag: 99, type: string, id: number, children: Array<Instance | TextInstance>, prop: any };
39 +type TextInstance = string;
40
41 var instanceCounter = 0;
42
42 -function recursivelyAppendChildren(flatArray : Array<Instance>, child : HostChildren<Instance>) {
43 +function recursivelyAppendChildren(flatArray : Array<Instance | TextInstance>, child : HostChildren<Instance | TextInstance>) {
44 if (!child) {
45 return;
46 }
46 - if (child.tag === TERMINAL_TAG) {
47 + if (typeof child === 'string' || child.tag === TERMINAL_TAG) {
48 flatArray.push(child);
49 } else {
50 let node = child;
@@ -53,7 +54,7 @@ function recursivelyAppendChildren(flatArray : Array<Instance>, child : HostChil
54 }
55 }
56
56 -function flattenChildren(children : HostChildren<Instance>) {
57 +function flattenChildren(children : HostChildren<Instance | TextInstance>) {
58 const flatArray = [];
59 recursivelyAppendChildren(flatArray, children);
60 return flatArray;
@@ -61,11 +62,11 @@ function flattenChildren(children : HostChildren<Instance>) {
62
63 var NoopRenderer = ReactFiberReconciler({
64
64 - updateContainer(containerInfo : Container, children : HostChildren<Instance>) : void {
65 + updateContainer(containerInfo : Container, children : HostChildren<Instance | TextInstance>) : void {
66 containerInfo.children = flattenChildren(children);
67 },
68
68 - createInstance(type : string, props : Props, children : HostChildren<Instance>) : Instance {
69 + createInstance(type : string, props : Props, children : HostChildren<Instance | TextInstance>) : Instance {
70 const inst = {
71 tag: TERMINAL_TAG,
72 id: instanceCounter++,
@@ -79,11 +80,11 @@ var NoopRenderer = ReactFiberReconciler({
80 return inst;
81 },
82
82 - prepareUpdate(instance : Instance, oldProps : Props, newProps : Props, children : HostChildren<Instance>) : boolean {
83 + prepareUpdate(instance : Instance, oldProps : Props, newProps : Props, children : HostChildren<Instance | TextInstance>) : boolean {
84 return true;
85 },
86
86 - commitUpdate(instance : Instance, oldProps : Props, newProps : Props, children : HostChildren<Instance>) : void {
87 + commitUpdate(instance : Instance, oldProps : Props, newProps : Props, children : HostChildren<Instance | TextInstance>) : void {
88 instance.children = flattenChildren(children);
89 instance.prop = newProps.prop;
90 },
@@ -91,6 +92,14 @@ var NoopRenderer = ReactFiberReconciler({
92 deleteInstance(instance : Instance) : void {
93 },
94
95 + createTextInstance(text : string) : TextInstance {
96 + return text;
97 + },
98 +
99 + commitTextUpdate(textInstance : TextInstance, oldText : string, newText : string) : void {
100 + // Not yet supported.
101 + },
102 +
103 scheduleAnimationCallback(callback) {
104 scheduledAnimationCallback = callback;
105 },
@@ -166,11 +175,15 @@ var ReactNoop = {
175 bufferedLog.push(...args, '\n');
176 }
177
169 - function logHostInstances(children: Array<Instance>, depth) {
178 + function logHostInstances(children: Array<Instance | TextInstance>, depth) {
179 for (var i = 0; i < children.length; i++) {
180 var child = children[i];
172 - log(' '.repeat(depth) + '- ' + child.type + '#' + child.id);
173 - logHostInstances(child.children, depth + 1);
181 + if (typeof child === 'string') {
182 + log(' '.repeat(depth) + '- ' + child);
183 + } else {
184 + log(' '.repeat(depth) + '- ' + child.type + '#' + child.id);
185 + logHostInstances(child.children, depth + 1);
186 + }
187 }
188 }
189 function logContainer(container : Container, depth) {
src/renderers/shared/fiber/ReactChildFiber.js
+14
@@ -31,6 +31,7 @@ const {
31 cloneFiber,
32 createFiberFromElement,
33 createFiberFromFragment,
34 + createFiberFromText,
35 createFiberFromCoroutine,
36 createFiberFromYield,
37 } = ReactFiber;
@@ -50,6 +51,13 @@ function ChildReconciler(shouldClone) {
51 newChildren : any,
52 priority : PriorityLevel
53 ) : Fiber {
54 + if (typeof newChildren === 'string') {
55 + const textNode = createFiberFromText(newChildren, priority);
56 + previousSibling.sibling = textNode;
57 + textNode.return = returnFiber;
58 + return textNode;
59 + }
60 +
61 if (typeof newChildren !== 'object' || newChildren === null) {
62 return previousSibling;
63 }
@@ -111,6 +119,12 @@ function ChildReconciler(shouldClone) {
119 }
120
121 function createFirstChild(returnFiber, existingChild, newChildren : any, priority) {
122 + if (typeof newChildren === 'string') {
123 + const textNode = createFiberFromText(newChildren, priority);
124 + textNode.return = returnFiber;
125 + return textNode;
126 + }
127 +
128 if (typeof newChildren !== 'object' || newChildren === null) {
129 return null;
130 }
src/renderers/shared/fiber/ReactFiber.js
+8
@@ -24,6 +24,7 @@ var {
24 ClassComponent,
25 HostContainer,
26 HostComponent,
27 + HostText,
28 CoroutineComponent,
29 YieldComponent,
30 Fragment,
@@ -256,6 +257,13 @@ exports.createFiberFromFragment = function(elements : ReactFragment, priorityLev
257 return fiber;
258 };
259
260 +exports.createFiberFromText = function(content : string, priorityLevel : PriorityLevel) {
261 + const fiber = createFiber(HostText, null);
262 + fiber.pendingProps = content;
263 + fiber.pendingWorkPriority = priorityLevel;
264 + return fiber;
265 +};
266 +
267 function createFiberFromElementType(type : mixed, key : null | string) {
268 let fiber;
269 if (typeof type === 'function') {
src/renderers/shared/fiber/ReactFiberBeginWork.js
+14 -2
@@ -33,6 +33,7 @@ var {
33 ClassComponent,
34 HostContainer,
35 HostComponent,
36 + HostText,
37 CoroutineComponent,
38 CoroutineHandlerPhase,
39 YieldComponent,
@@ -50,7 +51,7 @@ var {
51 } = require('ReactFiberUpdateQueue');
52 var ReactInstanceMap = require('ReactInstanceMap');
53
53 -module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getScheduler : () => Scheduler) {
54 +module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>, getScheduler : () => Scheduler) {
55
56 function markChildAsProgressed(current, workInProgress, priorityLevel) {
57 // We now have clones. Let's store them as the currently progressed work.
@@ -239,7 +240,14 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getSchedu
240 }
241
242 function updateHostComponent(current, workInProgress) {
242 - const nextChildren = workInProgress.pendingProps.children;
243 + let nextChildren = workInProgress.pendingProps.children;
244 + if (typeof nextChildren === 'string' || typeof nextChildren === 'number') {
245 + // We special case a direct text child of a host node. This is a common
246 + // case. We won't handle it as a reified child. We will instead handle
247 + // this in the host environment that also have access to this prop. That
248 + // avoids allocating another HostText fiber and traversing it.
249 + nextChildren = null;
250 + }
251 if (workInProgress.pendingProps.hidden &&
252 workInProgress.pendingWorkPriority !== OffscreenPriority) {
253 // If this host component is hidden, we can bail out on the children.
@@ -391,6 +399,10 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getSchedu
399 config.beginUpdate(workInProgress.stateNode);
400 }
401 return updateHostComponent(current, workInProgress);
402 + case HostText:
403 + // Nothing to do here. This is terminal. We'll do the completion step
404 + // immediately after.
405 + return null;
406 case CoroutineHandlerPhase:
407 // This is a restart. Reset the tag to the initial phase.
408 workInProgress.tag = CoroutineComponent;
src/renderers/shared/fiber/ReactFiberCommitWork.js
+15 -1
@@ -21,13 +21,15 @@ var {
21 ClassComponent,
22 HostContainer,
23 HostComponent,
24 + HostText,
25 } = ReactTypeOfWork;
26 var { callCallbacks } = require('ReactFiberUpdateQueue');
27
27 -module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
28 +module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
29
30 const updateContainer = config.updateContainer;
31 const commitUpdate = config.commitUpdate;
32 + const commitTextUpdate = config.commitTextUpdate;
33
34 function commitWork(current : ?Fiber, finishedWork : Fiber) : void {
35 switch (finishedWork.tag) {
@@ -68,6 +70,18 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
70 commitUpdate(instance, oldProps, newProps, children);
71 return;
72 }
73 + case HostText: {
74 + if (finishedWork.stateNode == null || !current) {
75 + throw new Error('This should only be done during updates.');
76 + }
77 + // TODO: This never gets called yet because I don't have update support
78 + // for text nodes. This only gets updated through a host component or
79 + // container updating with this as one of its child nodes.
80 + const textInstance : TI = finishedWork.stateNode;
81 + const oldText : string = finishedWork.memoizedProps;
82 + const newText : string = current.memoizedProps;
83 + commitTextUpdate(textInstance, oldText, newText);
84 + }
85 default:
86 throw new Error('This unit of work tag should not have side-effects.');
87 }
src/renderers/shared/fiber/ReactFiberCompleteWork.js
+25 -1
@@ -25,15 +25,17 @@ var {
25 ClassComponent,
26 HostContainer,
27 HostComponent,
28 + HostText,
29 CoroutineComponent,
30 CoroutineHandlerPhase,
31 YieldComponent,
32 Fragment,
33 } = ReactTypeOfWork;
34
34 -module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
35 +module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
36
37 const createInstance = config.createInstance;
38 + const createTextInstance = config.createTextInstance;
39 const prepareUpdate = config.prepareUpdate;
40
41 function markForPreEffect(workInProgress : Fiber) {
@@ -190,6 +192,28 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
192 }
193 workInProgress.memoizedProps = newProps;
194 return null;
195 + case HostText:
196 + let newText = workInProgress.pendingProps;
197 + if (current && workInProgress.stateNode != null) {
198 + // If we have an alternate, that means this is an update and we need to
199 + // schedule a side-effect to do the updates.
200 + markForPreEffect(workInProgress);
201 + } else {
202 + if (typeof newText !== 'string') {
203 + if (workInProgress.stateNode === null) {
204 + throw new Error('We must have new props for new mounts.');
205 + } else {
206 + // This can happen when we abort work.
207 + return null;
208 + }
209 + }
210 + const textInstance = createTextInstance(newText);
211 + // TODO: This seems like unnecessary duplication.
212 + workInProgress.stateNode = textInstance;
213 + workInProgress.output = textInstance;
214 + }
215 + workInProgress.memoizedProps = newText;
216 + return null;
217 case CoroutineComponent:
218 return moveCoroutineToHandlerPhase(current, workInProgress);
219 case CoroutineHandlerPhase:
src/renderers/shared/fiber/ReactFiberReconciler.js
+9 -6
@@ -28,19 +28,22 @@ type HostChildNode<I> = { tag: TypeOfWork, output: HostChildren<I>, sibling: any
28
29 export type HostChildren<I> = null | void | I | HostChildNode<I>;
30
31 -export type HostConfig<T, P, I, C> = {
31 +export type HostConfig<T, P, I, TI, C> = {
32
33 // TODO: We don't currently have a quick way to detect that children didn't
34 // reorder so we host will always need to check the set. We should make a flag
35 // or something so that it can bailout easily.
36
37 - updateContainer(containerInfo : C, children : HostChildren<I>) : void;
37 + updateContainer(containerInfo : C, children : HostChildren<I | TI>) : void;
38
39 - createInstance(type : T, props : P, children : HostChildren<I>) : I,
40 - prepareUpdate(instance : I, oldProps : P, newProps : P, children : HostChildren<I>) : boolean,
41 - commitUpdate(instance : I, oldProps : P, newProps : P, children : HostChildren<I>) : void,
39 + createInstance(type : T, props : P, children : HostChildren<I | TI>) : I,
40 + prepareUpdate(instance : I, oldProps : P, newProps : P, children : HostChildren<I | TI>) : boolean,
41 + commitUpdate(instance : I, oldProps : P, newProps : P, children : HostChildren<I | TI>) : void,
42 deleteInstance(instance : I) : void,
43
44 + createTextInstance(text : string) : TI,
45 + commitTextUpdate(textInstance : TI, oldText : string, newText : string) : void,
46 +
47 scheduleAnimationCallback(callback : () => void) : void,
48 scheduleDeferredCallback(callback : (deadline : Deadline) => void) : void
49
@@ -58,7 +61,7 @@ export type Reconciler<C> = {
61 getPublicRootInstance(container : OpaqueNode) : (C | null),
62 };
63
61 -module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) : Reconciler<C> {
64 +module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) : Reconciler<C> {
65
66 var { scheduleWork, performWithPriority } = ReactFiberScheduler(config);
67
src/renderers/shared/fiber/ReactFiberScheduler.js
+1 -1
@@ -36,7 +36,7 @@ export type Scheduler = {
36 scheduleDeferredWork: (root : FiberRoot, priority : PriorityLevel) => void
37 };
38
39 -module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
39 +module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
40 // Use a closure to circumvent the circular dependency between the scheduler
41 // and ReactFiberBeginWork. Don't know if there's a better way to do this.
42 let scheduler;
src/renderers/shared/fiber/ReactTypeOfWork.js
+6 -5
@@ -12,7 +12,7 @@
12
13 'use strict';
14
15 -export type TypeOfWork = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
15 +export type TypeOfWork = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
16
17 module.exports = {
18 IndeterminateComponent: 0, // Before we know whether it is functional or class
@@ -20,8 +20,9 @@ module.exports = {
20 ClassComponent: 2,
21 HostContainer: 3, // Root of a host tree. Could be nested inside another node.
22 HostComponent: 4,
23 - CoroutineComponent: 5,
24 - CoroutineHandlerPhase: 6,
25 - YieldComponent: 7,
26 - Fragment: 8,
23 + HostText: 5,
24 + CoroutineComponent: 6,
25 + CoroutineHandlerPhase: 7,
26 + YieldComponent: 8,
27 + Fragment: 9,
28 };
src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js
+32
@@ -89,6 +89,38 @@ describe('ReactIncrementalSideEffects', () => {
89
90 });
91
92 + it('can update child nodes rendering into text nodes', function() {
93 +
94 + function Bar(props) {
95 + return props.text;
96 + }
97 +
98 + function Foo(props) {
99 + return (
100 + <div>
101 + <Bar text={props.text} />
102 + {props.text === 'World' ? [
103 + <Bar key="a" text={props.text} />,
104 + '!',
105 + ] : null}
106 + </div>
107 + );
108 + }
109 +
110 + ReactNoop.render(<Foo text="Hello" />);
111 + ReactNoop.flush();
112 + expect(ReactNoop.root.children).toEqual([
113 + div('Hello'),
114 + ]);
115 +
116 + ReactNoop.render(<Foo text="World" />);
117 + ReactNoop.flush();
118 + expect(ReactNoop.root.children).toEqual([
119 + div('World', 'World', '!'),
120 + ]);
121 +
122 + });
123 +
124 it('does not update child nodes if a flush is aborted', () => {
125
126 function Bar(props) {