@samitouri / QOS-React-2 / commits / 5de2821372

Move child updates to use the reconciled effects

Instead of passing the full list of children every time to update the host environment, we'll only do inserts/deletes. We loop over all the placement effects first and then later we do the rest.

Sebastian Markbage committed Oct 5, 2016 at 02:20 UTC 5de2821372c431122e25df69f1ece530a79ec24e
9 files changed +258 -36
src/renderers/dom/fiber/ReactDOMFiber.js
+15 -9
@@ -46,6 +46,7 @@ function recursivelyAppendChildren(parent : Element, child : HostChildren<Instan
46 var DOMRenderer = ReactFiberReconciler({
47
48 updateContainer(container : Container, children : HostChildren<Instance | TextInstance>) : void {
49 + // TODO: Containers should update similarly to other parents.
50 container.innerHTML = '';
51 recursivelyAppendChildren(container, children);
52 },
@@ -63,25 +64,18 @@ var DOMRenderer = ReactFiberReconciler({
64 prepareUpdate(
65 domElement : Instance,
66 oldProps : Props,
66 - newProps : Props,
67 - children : HostChildren<Instance | TextInstance>
67 + newProps : Props
68 ) : boolean {
69 return true;
70 },
71
72 - commitUpdate(domElement : Instance, oldProps : Props, newProps : Props, children : HostChildren<Instance | TextInstance>) : void {
73 - domElement.innerHTML = '';
74 - recursivelyAppendChildren(domElement, children);
72 + commitUpdate(domElement : Instance, oldProps : Props, newProps : Props) : void {
73 if (typeof newProps.children === 'string' ||
74 typeof newProps.children === 'number') {
75 domElement.textContent = newProps.children;
76 }
77 },
78
81 - deleteInstance(instance : Instance) : void {
82 - // Noop
83 - },
84 -
79 createTextInstance(text : string) : TextInstance {
80 return document.createTextNode(text);
81 },
@@ -90,6 +84,18 @@ var DOMRenderer = ReactFiberReconciler({
84 textInstance.nodeValue = newText;
85 },
86
87 + appendChild(parentInstance : Instance, child : Instance | TextInstance) : void {
88 + parentInstance.appendChild(child);
89 + },
90 +
91 + insertBefore(parentInstance : Instance, child : Instance | TextInstance, beforeChild : Instance | TextInstance) : void {
92 + parentInstance.insertBefore(child, beforeChild);
93 + },
94 +
95 + removeChild(parentInstance : Instance, child : Instance | TextInstance) : void {
96 + parentInstance.removeChild(child);
97 + },
98 +
99 scheduleAnimationCallback: window.requestAnimationFrame,
100
101 scheduleDeferredCallback: window.requestIdleCallback,
src/renderers/noop/ReactNoop.js
+30 -6
@@ -81,18 +81,14 @@ var NoopRenderer = ReactFiberReconciler({
81 return inst;
82 },
83
84 - prepareUpdate(instance : Instance, oldProps : Props, newProps : Props, children : HostChildren<Instance | TextInstance>) : boolean {
84 + prepareUpdate(instance : Instance, oldProps : Props, newProps : Props) : boolean {
85 return true;
86 },
87
88 - commitUpdate(instance : Instance, oldProps : Props, newProps : Props, children : HostChildren<Instance | TextInstance>) : void {
89 - instance.children = flattenChildren(children);
88 + commitUpdate(instance : Instance, oldProps : Props, newProps : Props) : void {
89 instance.prop = newProps.prop;
90 },
91
93 - deleteInstance(instance : Instance) : void {
94 - },
95 -
92 createTextInstance(text : string) : TextInstance {
93 var inst = { tag: TEXT_TAG, text : text };
94 // Hide from unit tests
@@ -104,6 +100,34 @@ var NoopRenderer = ReactFiberReconciler({
100 textInstance.text = newText;
101 },
102
103 + appendChild(parentInstance : Instance, child : Instance | TextInstance) : void {
104 + const index = parentInstance.children.indexOf(child);
105 + if (index !== -1) {
106 + parentInstance.children.splice(index, 1);
107 + }
108 + parentInstance.children.push(child);
109 + },
110 +
111 + insertBefore(parentInstance : Instance, child : Instance | TextInstance, beforeChild : Instance | TextInstance) : void {
112 + const index = parentInstance.children.indexOf(child);
113 + if (index !== -1) {
114 + parentInstance.children.splice(index, 1);
115 + }
116 + const beforeIndex = parentInstance.children.indexOf(beforeChild);
117 + if (beforeIndex === -1) {
118 + throw new Error('This child does not exist.');
119 + }
120 + parentInstance.children.splice(beforeIndex, 0, child);
121 + },
122 +
123 + removeChild(parentInstance : Instance, child : Instance | TextInstance) : void {
124 + const index = parentInstance.children.indexOf(child);
125 + if (index === -1) {
126 + throw new Error('This child does not exist.');
127 + }
128 + parentInstance.children.splice(index, 1);
129 + },
130 +
131 scheduleAnimationCallback(callback) {
132 scheduledAnimationCallback = callback;
133 },
src/renderers/shared/fiber/ReactChildFiber.js
+17 -8
@@ -183,6 +183,15 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
183 }
184 }
185
186 + function placeSingleChild(newFiber : Fiber) {
187 + // This is simpler for the single child case. We only need to do a
188 + // placement for inserting new children.
189 + if (shouldTrackSideEffects && !newFiber.alternate) {
190 + newFiber.effectTag = Placement;
191 + }
192 + return newFiber;
193 + }
194 +
195 function updateTextNode(
196 returnFiber : Fiber,
197 current : ?Fiber,
@@ -738,39 +747,39 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
747 // fragment nodes. Recursion happens at the normal flow.
748
749 if (typeof newChild === 'string' || typeof newChild === 'number') {
741 - return reconcileSingleTextNode(
750 + return placeSingleChild(reconcileSingleTextNode(
751 returnFiber,
752 currentFirstChild,
753 '' + newChild,
754 priority
746 - );
755 + ));
756 }
757
758 if (typeof newChild === 'object' && newChild !== null) {
759 switch (newChild.$$typeof) {
760 case REACT_ELEMENT_TYPE:
752 - return reconcileSingleElement(
761 + return placeSingleChild(reconcileSingleElement(
762 returnFiber,
763 currentFirstChild,
764 newChild,
765 priority
757 - );
766 + ));
767
768 case REACT_COROUTINE_TYPE:
760 - return reconcileSingleCoroutine(
769 + return placeSingleChild(reconcileSingleCoroutine(
770 returnFiber,
771 currentFirstChild,
772 newChild,
773 priority
765 - );
774 + ));
775
776 case REACT_YIELD_TYPE:
768 - return reconcileSingleYield(
777 + return placeSingleChild(reconcileSingleYield(
778 returnFiber,
779 currentFirstChild,
780 newChild,
781 priority
773 - );
782 + ));
783 }
784
785 if (isArray(newChild)) {
src/renderers/shared/fiber/ReactFiberBeginWork.js
+17
@@ -50,6 +50,9 @@ var {
50 addCallbackToQueue,
51 mergeUpdateQueue,
52 } = require('ReactFiberUpdateQueue');
53 +var {
54 + Placement,
55 +} = require('ReactTypeOfSideEffect');
56 var ReactInstanceMap = require('ReactInstanceMap');
57
58 module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>, getScheduler : () => Scheduler) {
@@ -299,6 +302,20 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>, g
302 // Reconcile the children and stash them for later work.
303 reconcileChildrenAtPriority(current, workInProgress, nextChildren, OffscreenPriority);
304 workInProgress.child = current ? current.child : null;
305 +
306 + if (!current) {
307 + // If this doesn't have a current we won't track it for placement
308 + // effects. However, when we come back around to this we have already
309 + // inserted the parent which means that we'll infact need to make this a
310 + // placement.
311 + // TODO: There has to be a better solution to this problem.
312 + let child = workInProgress.progressedChild;
313 + while (child) {
314 + child.effectTag = Placement;
315 + child = child.sibling;
316 + }
317 + }
318 +
319 // Abort and don't process children yet.
320 return null;
321 } else {
src/renderers/shared/fiber/ReactFiberCommitWork.js
+137 -3
@@ -25,12 +25,146 @@ var {
25 } = ReactTypeOfWork;
26 var { callCallbacks } = require('ReactFiberUpdateQueue');
27
28 +var {
29 + Placement,
30 + PlacementAndUpdate,
31 +} = require('ReactTypeOfSideEffect');
32 +
33 module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
34
35 const updateContainer = config.updateContainer;
36 const commitUpdate = config.commitUpdate;
37 const commitTextUpdate = config.commitTextUpdate;
38
39 + const appendChild = config.appendChild;
40 + const insertBefore = config.insertBefore;
41 + const removeChild = config.removeChild;
42 +
43 + function getHostParent(fiber : Fiber) : ?I {
44 + let parent = fiber.return;
45 + while (parent) {
46 + switch (parent.tag) {
47 + case HostComponent:
48 + return parent.stateNode;
49 + case HostContainer:
50 + // TODO: Currently we use the updateContainer feature to update these,
51 + // but we should be able to handle this case too.
52 + return null;
53 + }
54 + parent = parent.return;
55 + }
56 + return null;
57 + }
58 +
59 + function getHostSibling(fiber : Fiber) : ?I {
60 + // We're going to search forward into the tree until we find a sibling host
61 + // node. Unfortunately, if multiple insertions are done in a row we have to
62 + // search past them. This leads to exponential search for the next sibling.
63 + // TODO: Find a more efficient way to do this.
64 + let node : Fiber = fiber;
65 + siblings: while (true) {
66 + // If we didn't find anything, let's try the next sibling.
67 + while (!node.sibling) {
68 + if (!node.return || node.return.tag === HostComponent) {
69 + // If we pop out of the root or hit the parent the fiber we are the
70 + // last sibling.
71 + return null;
72 + }
73 + node = node.return;
74 + }
75 + node = node.sibling;
76 + while (node.tag !== HostComponent && node.tag !== HostText) {
77 + // If it is not host node and, we might have a host node inside it.
78 + // Try to search down until we find one.
79 + // TODO: For coroutines, this will have to search the stateNode.
80 + if (node.effectTag === Placement ||
81 + node.effectTag === PlacementAndUpdate) {
82 + // If we don't have a child, try the siblings instead.
83 + continue siblings;
84 + }
85 + if (!node.child) {
86 + continue siblings;
87 + } else {
88 + node = node.child;
89 + }
90 + }
91 + // Check if this host node is stable or about to be placed.
92 + if (node.effectTag !== Placement &&
93 + node.effectTag !== PlacementAndUpdate) {
94 + // Found it!
95 + return node.stateNode;
96 + }
97 + }
98 + }
99 +
100 + function commitInsertion(finishedWork : Fiber) : void {
101 + // Recursively insert all host nodes into the parent.
102 + const parent = getHostParent(finishedWork);
103 + if (!parent) {
104 + return;
105 + }
106 + const before = getHostSibling(finishedWork);
107 + // We only have the top Fiber that was inserted but we need recurse down its
108 + // children to find all the terminal nodes.
109 + let node : Fiber = finishedWork;
110 + while (true) {
111 + if (node.tag === HostComponent || node.tag === HostText) {
112 + if (before) {
113 + insertBefore(parent, node.stateNode, before);
114 + } else {
115 + appendChild(parent, node.stateNode);
116 + }
117 + } else if (node.child) {
118 + // TODO: Coroutines need to visit the stateNode.
119 + node = node.child;
120 + continue;
121 + }
122 + if (node === finishedWork) {
123 + return;
124 + }
125 + while (!node.sibling) {
126 + if (!node.return || node.return === finishedWork) {
127 + return;
128 + }
129 + node = node.return;
130 + }
131 + node = node.sibling;
132 + }
133 + }
134 +
135 + function commitDeletion(current : Fiber) : void {
136 + // Recursively delete all host nodes from the parent.
137 + const parent = getHostParent(current);
138 + if (!parent) {
139 + return;
140 + }
141 + // We only have the top Fiber that was inserted but we need recurse down its
142 + // children to find all the terminal nodes.
143 + // TODO: Call componentWillUnmount on all classes as needed. Recurse down
144 + // removed HostComponents but don't call removeChild on already removed
145 + // children.
146 + let node : Fiber = current;
147 + while (true) {
148 + if (node.tag === HostComponent || node.tag === HostText) {
149 + removeChild(parent, node.stateNode);
150 + } else if (node.child) {
151 + // TODO: Coroutines need to visit the stateNode.
152 + node = node.child;
153 + continue;
154 + }
155 + if (node === current) {
156 + return;
157 + }
158 + while (!node.sibling) {
159 + if (!node.return || node.return === current) {
160 + return;
161 + }
162 + node = node.return;
163 + }
164 + node = node.sibling;
165 + }
166 + }
167 +
168 function commitWork(current : ?Fiber, finishedWork : Fiber) : void {
169 switch (finishedWork.tag) {
170 case ClassComponent: {
@@ -62,12 +196,10 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
196 throw new Error('This should only be done during updates.');
197 }
198 // Commit the work prepared earlier.
65 - const child = finishedWork.child;
66 - const children = (child && !child.sibling) ? (child.output : ?Fiber | I) : child;
199 const newProps = finishedWork.memoizedProps;
200 const oldProps = current.memoizedProps;
201 const instance : I = finishedWork.stateNode;
70 - commitUpdate(instance, oldProps, newProps, children);
202 + commitUpdate(instance, oldProps, newProps);
203 return;
204 }
205 case HostText: {
@@ -86,6 +218,8 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
218 }
219
220 return {
221 + commitInsertion,
222 + commitDeletion,
223 commitWork,
224 };
225
src/renderers/shared/fiber/ReactFiberCompleteWork.js
+3 -3
@@ -142,8 +142,6 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
142 return null;
143 case HostComponent:
144 let newProps = workInProgress.pendingProps;
145 - const child = workInProgress.child;
146 - const children = (child && !child.sibling) ? (child.output : ?Fiber | I) : child;
145 if (current && workInProgress.stateNode != null) {
146 // If we have an alternate, that means this is an update and we need to
147 // schedule a side-effect to do the updates.
@@ -156,7 +154,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
154 newProps = oldProps;
155 }
156 const instance : I = workInProgress.stateNode;
159 - if (prepareUpdate(instance, oldProps, newProps, children)) {
157 + if (prepareUpdate(instance, oldProps, newProps)) {
158 // This returns true if there was something to update.
159 markUpdate(workInProgress);
160 }
@@ -171,6 +169,8 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
169 return null;
170 }
171 }
172 + const child = workInProgress.child;
173 + const children = (child && !child.sibling) ? (child.output : ?Fiber | I) : child;
174 const instance = createInstance(workInProgress.type, newProps, children);
175 // TODO: This seems like unnecessary duplication.
176 workInProgress.stateNode = instance;
src/renderers/shared/fiber/ReactFiberReconciler.js
+7 -4
@@ -34,16 +34,19 @@ export type HostConfig<T, P, I, TI, C> = {
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 | TI>) : void;
37 + updateContainer(containerInfo : C, children : HostChildren<I | TI>) : void,
38
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,
40 + prepareUpdate(instance : I, oldProps : P, newProps : P) : boolean,
41 + commitUpdate(instance : I, oldProps : P, newProps : P) : void,
42
43 createTextInstance(text : string) : TI,
44 commitTextUpdate(textInstance : TI, oldText : string, newText : string) : void,
45
46 + appendChild(parentInstance : I, child : I | TI) : void,
47 + insertBefore(parentInstance : I, child : I | TI, beforeChild : I | TI) : void,
48 + removeChild(parentInstance : I, child : I | TI) : void,
49 +
50 scheduleAnimationCallback(callback : () => void) : void,
51 scheduleDeferredCallback(callback : (deadline : Deadline) => void) : void
52
src/renderers/shared/fiber/ReactFiberScheduler.js
+20 -1
@@ -32,8 +32,10 @@ var {
32
33 var {
34 NoEffect,
35 + Placement,
36 Update,
37 PlacementAndUpdate,
38 + Deletion,
39 } = require('ReactTypeOfSideEffect');
40
41 var timeHeuristicForUnitOfWork = 1;
@@ -52,7 +54,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
54
55 const { beginWork } = ReactFiberBeginWork(config, getScheduler);
56 const { completeWork } = ReactFiberCompleteWork(config);
55 - const { commitWork } = ReactFiberCommitWork(config);
57 + const { commitInsertion, commitDeletion, commitWork } = ReactFiberCommitWork(config);
58
59 const scheduleAnimationCallback = config.scheduleAnimationCallback;
60 const scheduleDeferredCallback = config.scheduleDeferredCallback;
@@ -109,7 +111,24 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
111 function commitAllWork(finishedWork : Fiber) {
112 // Commit all the side-effects within a tree.
113 // TODO: Error handling.
114 +
115 + // First, we'll perform all the host insertion and deletion effects.
116 let effectfulFiber = finishedWork.firstEffect;
117 + while (effectfulFiber) {
118 + switch (effectfulFiber.effectTag) {
119 + case Placement:
120 + case PlacementAndUpdate:
121 + commitInsertion(effectfulFiber);
122 + break;
123 + case Deletion:
124 + commitDeletion(effectfulFiber);
125 + break;
126 + }
127 + effectfulFiber = effectfulFiber.nextEffect;
128 + }
129 +
130 + // Next, we'll perform all other effects.
131 + effectfulFiber = finishedWork.firstEffect;
132 while (effectfulFiber) {
133 const current = effectfulFiber.alternate;
134 if (effectfulFiber.effectTag === Update ||
src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js
+12 -2
@@ -71,7 +71,11 @@ describe('ReactIncrementalSideEffects', () => {
71 {props.text === 'World' ? [
72 <Bar key="a" text={props.text} />,
73 <div key="b" />,
74 + ] : props.text === 'Hi' ? [
75 + <div key="b" />,
76 + <Bar key="a" text={props.text} />,
77 ] : null}
78 + <span prop="test" />
79 </div>
80 );
81 }
@@ -79,13 +83,19 @@ describe('ReactIncrementalSideEffects', () => {
83 ReactNoop.render(<Foo text="Hello" />);
84 ReactNoop.flush();
85 expect(ReactNoop.root.children).toEqual([
82 - div(span()),
86 + div(span(), span('test')),
87 ]);
88
89 ReactNoop.render(<Foo text="World" />);
90 ReactNoop.flush();
91 expect(ReactNoop.root.children).toEqual([
88 - div(span(), span(), div()),
92 + div(span(), span(), div(), span('test')),
93 + ]);
94 +
95 + ReactNoop.render(<Foo text="Hi" />);
96 + ReactNoop.flush();
97 + expect(ReactNoop.root.children).toEqual([
98 + div(span(), div(), span(), span('test')),
99 ]);
100
101 });