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
});