14
* limitations under the License.
15
*
16
* @providesModule ReactMultiChild
17
+ * @typechecks static-only
18
*/
19
20
"use strict";
21
22
var ReactComponent = require('ReactComponent');
23
+var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes');
24
25
/**
24
- * Given a `curChild` and `newChild`, determines if `curChild` should be managed
25
- * as it exists, as opposed to being destroyed and/or replaced.
26
+ * Given a `curChild` and `newChild`, determines if `curChild` should be
27
+ * updated as opposed to being destroyed or replaced.
28
+ *
29
* @param {?ReactComponent} curChild
30
* @param {?ReactComponent} newChild
28
- * @return {!boolean} Whether or not `curChild` should be updated with
29
- * `newChild`'s props
31
+ * @return {boolean} True if `curChild` should be updated with `newChild`.
32
+ * @protected
33
*/
31
-function shouldManageExisting(curChild, newChild) {
34
+function shouldUpdateChild(curChild, newChild) {
35
return curChild && newChild && curChild.constructor === newChild.constructor;
36
}
37
38
/**
36
- * `ReactMultiChild` provides common functionality for components that have
37
- * multiple children. Standard `ReactCompositeComponent`s do not currently have
38
- * multiple children. `ReactNativeComponent`s do, however. Other specially
39
- * reconciled components will also have multiple children. Contains three
40
- * internally used properties that are used to keep track of state throughout
41
- * the `updateMultiChild` process.
39
+ * Updating children of a component may trigger recursive updates. The depth is
40
+ * used to batch recursive updates to render markup more efficiently.
41
*
43
- * @class ReactMultiChild
42
+ * @type {number}
43
+ * @private
44
*/
45
+var updateDepth = 0;
46
47
/**
47
- * @lends `ReactMultiChildMixin`.
48
+ * Queue of update configuration objects.
49
+ *
50
+ * Each object has a `type` property that is in `ReactMultiChildUpdateTypes`.
51
+ *
52
+ * @type {array<object>}
53
+ * @private
54
*/
49
-var ReactMultiChildMixin = {
50
-
51
- enqueueMarkupAt: function(markup, insertAt) {
52
- this.domOperations = this.domOperations || [];
53
- this.domOperations.push({insertMarkup: markup, finalIndex: insertAt});
54
- },
55
-
56
- enqueueMove: function(originalIndex, finalIndex) {
57
- this.domOperations = this.domOperations || [];
58
- this.domOperations.push({moveFrom: originalIndex, finalIndex: finalIndex});
59
- },
60
-
61
- enqueueUnmountChildByName: function(name, removeChild) {
62
- if (ReactComponent.isValidComponent(removeChild)) {
63
- this.domOperations = this.domOperations || [];
64
- this.domOperations.push({removeAt: removeChild._domIndex});
65
- removeChild.unmountComponent && removeChild.unmountComponent();
66
- delete this._renderedChildren[name];
67
- }
68
- },
55
+var updateQueue = [];
56
+
57
+/**
58
+ * Queue of markup to be rendered.
59
+ *
60
+ * @type {array<string>}
61
+ * @private
62
+ */
63
+var markupQueue = [];
64
+
65
+/**
66
+ * Enqueues markup to be rendered and inserted at a supplied index.
67
+ *
68
+ * @param {string} parentID ID of the parent component.
69
+ * @param {string} markup Markup that renders into an element.
70
+ * @param {number} toIndex Destination index.
71
+ * @private
72
+ */
73
+function enqueueMarkup(parentID, markup, toIndex) {
74
+ // NOTE: Null values reduce hidden classes.
75
+ updateQueue.push({
76
+ parentID: parentID,
77
+ parentNode: null,
78
+ type: ReactMultiChildUpdateTypes.INSERT_MARKUP,
79
+ markupIndex: markupQueue.push(markup) - 1,
80
+ fromIndex: null,
81
+ toIndex: toIndex
82
+ });
83
+}
84
+
85
+/**
86
+ * Enqueues moving an existing element to another index.
87
+ *
88
+ * @param {string} parentID ID of the parent component.
89
+ * @param {number} fromIndex Source index of the existing element.
90
+ * @param {number} toIndex Destination index of the element.
91
+ * @private
92
+ */
93
+function enqueueMove(parentID, fromIndex, toIndex) {
94
+ // NOTE: Null values reduce hidden classes.
95
+ updateQueue.push({
96
+ parentID: parentID,
97
+ parentNode: null,
98
+ type: ReactMultiChildUpdateTypes.MOVE_EXISTING,
99
+ markupIndex: null,
100
+ fromIndex: fromIndex,
101
+ toIndex: toIndex
102
+ });
103
+}
104
+
105
+/**
106
+ * Enqueues removing an element at an index.
107
+ *
108
+ * @param {string} parentID ID of the parent component.
109
+ * @param {number} fromIndex Index of the element to remove.
110
+ * @private
111
+ */
112
+function enqueueRemove(parentID, fromIndex) {
113
+ // NOTE: Null values reduce hidden classes.
114
+ updateQueue.push({
115
+ parentID: parentID,
116
+ parentNode: null,
117
+ type: ReactMultiChildUpdateTypes.REMOVE_NODE,
118
+ markupIndex: null,
119
+ fromIndex: fromIndex,
120
+ toIndex: null
121
+ });
122
+}
123
+
124
+/**
125
+ * Processes any enqueued updates.
126
+ *
127
+ * @private
128
+ */
129
+function processQueue() {
130
+ if (updateQueue.length) {
131
+ ReactComponent.DOMIDOperations.dangerouslyProcessChildrenUpdates(
132
+ updateQueue,
133
+ markupQueue
134
+ );
135
+ clearQueue();
136
+ }
137
+}
138
+
139
+/**
140
+ * Clears any enqueued updates.
141
+ *
142
+ * @private
143
+ */
144
+function clearQueue() {
145
+ updateQueue.length = 0;
146
+ markupQueue.length = 0;
147
+}
148
+
149
+/**
150
+ * ReactMultiChild are capable of reconciling multiple children.
151
+ *
152
+ * @class ReactMultiChild
153
+ * @internal
154
+ */
155
+var ReactMultiChild = {
156
157
/**
71
- * Process any pending DOM operations that have been accumulated when updating
72
- * the UI. By default, we execute the injected `DOMIDOperations` module's
73
- * `manageChildrenByParentID` which does executes the DOM operations without
74
- * any animation. It can be used as a reference implementation for special
75
- * animation based implementations.
158
+ * Provides common functionality for components that must reconcile multiple
159
+ * children. This is used by `ReactNativeComponent` to mount, update, and
160
+ * unmount child components.
161
*
77
- * @abstract
162
+ * @lends {ReactMultiChild.prototype}
163
*/
79
- processChildDOMOperationsQueue: function() {
80
- if (this.domOperations) {
81
- ReactComponent.DOMIDOperations
82
- .manageChildrenByParentID(this._rootNodeID, this.domOperations);
83
- this.domOperations = null;
84
- }
85
- },
164
+ Mixin: {
165
87
- unmountMultiChild: function() {
88
- var renderedChildren = this._renderedChildren;
89
- for (var name in renderedChildren) {
90
- if (renderedChildren.hasOwnProperty(name) && renderedChildren[name]) {
91
- var renderedChild = renderedChildren[name];
92
- renderedChild.unmountComponent && renderedChild.unmountComponent();
166
+ /**
167
+ * Generates a "mount image" for each of the supplied children. In the case
168
+ * of `ReactNativeComponent`, a mount image is a string of markup.
169
+ *
170
+ * @param {?object} children As returned by `flattenChildren`.
171
+ * @return {array} An array of mounted representations.
172
+ * @internal
173
+ */
174
+ mountChildren: function(children, transaction) {
175
+ var mountImages = [];
176
+ var index = 0;
177
+ for (var name in children) {
178
+ var child = children[name];
179
+ if (children.hasOwnProperty(name) && child) {
180
+ var mountImage = child.mountComponent(
181
+ // Inlined for performance, see `ReactID.createReactID`.
182
+ this._rootNodeID + '.' + name,
183
+ transaction
184
+ );
185
+ child._mountImage = mountImage;
186
+ child._mountIndex = index;
187
+ mountImages.push(mountImage);
188
+ index++;
189
+ }
190
}
94
- }
95
- this._renderedChildren = null;
96
- },
191
+ this._renderedChildren = children;
192
+ return mountImages;
193
+ },
194
98
- /**
99
- * Generates markup for a component that holds multiple children. #todo: Allow
100
- * all `ReactMultiChildMixin`s to support having arrays of children without a
101
- * container node. This current implementation may assume that children exist
102
- * at domIndices [0, parentNode.length].
103
- *
104
- * Has side effects of (likely) causing events to be registered. Also, every
105
- * component instance may only be rendered once.
106
- *
107
- * @param {?Object} children Flattened children object.
108
- * @return {!String} The rendered markup.
109
- */
110
- mountMultiChild: function(children, transaction) {
111
- var accum = '';
112
- var index = 0;
113
- for (var name in children) {
114
- var child = children[name];
115
- if (children.hasOwnProperty(name) && child) {
116
- accum += child.mountComponent(
117
- this._rootNodeID + '.' + name,
118
- transaction
119
- );
120
- child._domIndex = index;
121
- index++;
195
+ /**
196
+ * Updates the rendered children with new children.
197
+ *
198
+ * @param {?object} nextChildren As returned by `flattenChildren`.
199
+ * @param {ReactReconcileTransaction} transaction
200
+ * @internal
201
+ */
202
+ updateChildren: function(nextChildren, transaction) {
203
+ updateDepth++;
204
+ try {
205
+ this._updateChildren(nextChildren, transaction);
206
+ } catch (error) {
207
+ updateDepth--;
208
+ updateDepth || clearQueue();
209
+ throw error;
210
}
123
- }
124
- this._renderedChildren = children; // children are in just the right form!
125
- this.domOperations = null;
126
- return accum;
127
- },
211
+ updateDepth--;
212
+ updateDepth || processQueue();
213
+ },
214
129
- /**
130
- * Reconciles new children with old children in three phases.
131
- *
132
- * - Adds new content while updating existing children that should remain.
133
- * - Remove children that are no longer present in the next children.
134
- * - As a very last step, moves existing dom structures around.
135
- * - (Comment 1) `curChildrenDOMIndex` is the largest index of the current
136
- * rendered children that appears in the next children and did not need to
137
- * be "moved".
138
- * - (Comment 2) This is the key insight. If any non-removed child's previous
139
- * index is less than `curChildrenDOMIndex` it must be moved.
140
- *
141
- * @param {?Object} children Flattened children object.
142
- */
143
- updateMultiChild: function(nextChildren, transaction) {
144
- if (!nextChildren && !this._renderedChildren) {
145
- return;
146
- } else if (nextChildren && !this._renderedChildren) {
147
- this._renderedChildren = {}; // lazily allocate backing store with nothing
148
- } else if (!nextChildren && this._renderedChildren) {
149
- nextChildren = {};
150
- }
151
- var rootDomIdDot = this._rootNodeID + '.';
152
- var markupBuffer = null; // Accumulate adjacent new children markup.
153
- var numPendingInsert = 0; // How many root nodes are waiting in markupBuffer
154
- var loopDomIndex = 0; // Index of loop through new children.
155
- var curChildrenDOMIndex = 0; // See (Comment 1)
156
- for (var name in nextChildren) {
157
- if (!nextChildren.hasOwnProperty(name)) {continue;}
158
- var curChild = this._renderedChildren[name];
159
- var nextChild = nextChildren[name];
160
- if (shouldManageExisting(curChild, nextChild)) {
161
- if (markupBuffer) {
162
- this.enqueueMarkupAt(markupBuffer, loopDomIndex - numPendingInsert);
163
- markupBuffer = null;
215
+ /**
216
+ * Improve performance by isolating this hot code path from the try/catch
217
+ * block in `updateChildren`.
218
+ *
219
+ * @param {?object} nextChildren As returned by `flattenChildren`.
220
+ * @param {ReactReconcileTransaction} transaction
221
+ * @final
222
+ * @protected
223
+ */
224
+ _updateChildren: function(nextChildren, transaction) {
225
+ var prevChildren = this._renderedChildren;
226
+ if (!nextChildren && !prevChildren) {
227
+ return;
228
+ }
229
+ var name;
230
+ // `nextIndex` will increment for each child in `nextChildren`, but
231
+ // `lastIndex` will be the last index visited in `prevChildren`.
232
+ var lastIndex = 0;
233
+ var nextIndex = 0;
234
+ for (name in nextChildren) {
235
+ if (!nextChildren.hasOwnProperty(name)) {
236
+ continue;
237
}
165
- numPendingInsert = 0;
166
- if (curChild._domIndex < curChildrenDOMIndex) { // (Comment 2)
167
- this.enqueueMove(curChild._domIndex, loopDomIndex);
238
+ var prevChild = prevChildren && prevChildren[name];
239
+ var nextChild = nextChildren[name];
240
+ if (shouldUpdateChild(prevChild, nextChild)) {
241
+ this.moveChild(prevChild, nextIndex, lastIndex);
242
+ lastIndex = Math.max(prevChild._mountIndex, lastIndex);
243
+ prevChild.receiveProps(nextChild.props, transaction);
244
+ prevChild._mountIndex = nextIndex;
245
+ } else {
246
+ if (prevChild) {
247
+ this._unmountChildByName(prevChild, name);
248
+ lastIndex = Math.max(prevChild._mountIndex, lastIndex);
249
+ }
250
+ if (nextChild) {
251
+ this._mountChildByNameAtIndex(
252
+ nextChild, name, nextIndex, transaction
253
+ );
254
+ }
255
}
169
- curChildrenDOMIndex = Math.max(curChild._domIndex, curChildrenDOMIndex);
170
- curChild.receiveProps(nextChild.props, transaction);
171
- curChild._domIndex = loopDomIndex;
172
- } else {
173
- if (curChild) { // !shouldUpdate && curChild => delete
174
- this.enqueueUnmountChildByName(name, curChild);
175
- curChildrenDOMIndex =
176
- Math.max(curChild._domIndex, curChildrenDOMIndex);
256
+ if (nextChild) {
257
+ nextIndex++;
258
}
178
- if (nextChild) { // !shouldUpdate && nextChild => insert
179
- this._renderedChildren[name] = nextChild;
180
- var nextMarkup =
181
- nextChild.mountComponent(rootDomIdDot + name, transaction);
182
- markupBuffer = markupBuffer ? markupBuffer + nextMarkup : nextMarkup;
183
- numPendingInsert++;
184
- nextChild._domIndex = loopDomIndex;
259
+ }
260
+ // Remove children that are no longer present.
261
+ for (name in prevChildren) {
262
+ if (prevChildren.hasOwnProperty(name) &&
263
+ prevChildren[name] &&
264
+ !(nextChildren && nextChildren[name])) {
265
+ this._unmountChildByName(prevChildren[name], name);
266
}
267
}
187
- loopDomIndex = nextChild ? loopDomIndex + 1 : loopDomIndex;
188
- }
189
- if (markupBuffer) {
190
- this.enqueueMarkupAt(markupBuffer, loopDomIndex - numPendingInsert);
191
- }
192
- for (var childName in this._renderedChildren) { // from other direction
193
- if (!this._renderedChildren.hasOwnProperty(childName)) { continue; }
194
- var child = this._renderedChildren[childName];
195
- if (child && !nextChildren[childName]) {
196
- this.enqueueUnmountChildByName(childName, child);
268
+ },
269
+
270
+ /**
271
+ * Unmounts all rendered children. This should be used to clean up children
272
+ * when this component is unmounted.
273
+ *
274
+ * @internal
275
+ */
276
+ unmountChildren: function() {
277
+ var renderedChildren = this._renderedChildren;
278
+ for (var name in renderedChildren) {
279
+ var renderedChild = renderedChildren[name];
280
+ if (renderedChild && renderedChild.unmountComponent) {
281
+ renderedChild.unmountComponent();
282
+ }
283
+ }
284
+ this._renderedChildren = null;
285
+ },
286
+
287
+ /**
288
+ * Moves a child component to the supplied index.
289
+ *
290
+ * @param {ReactComponent} child Component to move.
291
+ * @param {number} toIndex Destination index of the element.
292
+ * @param {number} lastIndex Last index visited of the siblings of `child`.
293
+ * @protected
294
+ */
295
+ moveChild: function(child, toIndex, lastIndex) {
296
+ // If the index of `child` is less than `lastIndex`, then it needs to
297
+ // be moved. Otherwise, we do not need to move it because a child will be
298
+ // inserted or moved before `child`.
299
+ if (child._mountIndex < lastIndex) {
300
+ enqueueMove(this._rootNodeID, child._mountIndex, toIndex);
301
+ }
302
+ },
303
+
304
+ /**
305
+ * Creates a child component.
306
+ *
307
+ * @param {ReactComponent} child Component to create.
308
+ * @protected
309
+ */
310
+ createChild: function(child) {
311
+ enqueueMarkup(this._rootNodeID, child._mountImage, child._mountIndex);
312
+ },
313
+
314
+ /**
315
+ * Removes a child component.
316
+ *
317
+ * @param {ReactComponent} child Child to remove.
318
+ * @protected
319
+ */
320
+ removeChild: function(child) {
321
+ enqueueRemove(this._rootNodeID, child._mountIndex);
322
+ },
323
+
324
+ /**
325
+ * Mounts a child with the supplied name.
326
+ *
327
+ * NOTE: This is part of `updateChildren` and is here for readability.
328
+ *
329
+ * @param {ReactComponent} child Component to mount.
330
+ * @param {string} name Name of the child.
331
+ * @param {number} index Index at which to insert the child.
332
+ * @param {ReactReconcileTransaction} transaction
333
+ * @private
334
+ */
335
+ _mountChildByNameAtIndex: function(child, name, index, transaction) {
336
+ // Inlined for performance, see `ReactID.createReactID`.
337
+ var rootID = this._rootNodeID + '.' + name;
338
+ var mountImage = child.mountComponent(rootID, transaction);
339
+ child._mountImage = mountImage;
340
+ child._mountIndex = index;
341
+ this.createChild(child);
342
+ this._renderedChildren = this._renderedChildren || {};
343
+ this._renderedChildren[name] = child;
344
+ },
345
+
346
+ /**
347
+ * Unmounts a rendered child by name.
348
+ *
349
+ * NOTE: This is part of `updateChildren` and is here for readability.
350
+ *
351
+ * @param {ReactComponent} child Component to unmount.
352
+ * @param {string} name Name of the child in `this._renderedChildren`.
353
+ * @private
354
+ */
355
+ _unmountChildByName: function(child, name) {
356
+ if (ReactComponent.isValidComponent(child)) {
357
+ this.removeChild(child);
358
+ child._mountImage = null;
359
+ child._mountIndex = null;
360
+ child.unmountComponent();
361
+ delete this._renderedChildren[name];
362
}
363
}
199
- this.processChildDOMOperationsQueue();
364
+
365
}
201
-};
366
203
-var ReactMultiChild = {
204
- Mixin: ReactMultiChildMixin
367
};
368
369
module.exports = ReactMultiChild;