mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Move nodes around by reference instead of by index
This makes things easier if we ever want to use more than one DOM node for a component. Notably, this is more convenient if we want to remove the wrappers around text components (since text nodes can be split and joined however a browser feels like) or if we want to support returning more than one element from render (#2127). I left the old indexes so that implementations aren't forced to use the node/image if they prefer indices, because I'm not sure yet whether the changes corresponding to my rewrite of DOMChildrenOperations are easy or hard yet in React Native. (The tests pass with and without the DOMChildrenOperations changes here.)
This commit is contained in:
@@ -18,7 +18,10 @@ var ReactPerf = require('ReactPerf');
|
||||
|
||||
var setInnerHTML = require('setInnerHTML');
|
||||
var setTextContent = require('setTextContent');
|
||||
var invariant = require('invariant');
|
||||
|
||||
function getNodeAfter(parentNode, node) {
|
||||
return node ? node.nextSibling : parentNode.firstChild;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts `childNode` as a child of `parentNode` at the `index`.
|
||||
@@ -28,27 +31,14 @@ var invariant = require('invariant');
|
||||
* @param {number} index Index at which to insert the child.
|
||||
* @internal
|
||||
*/
|
||||
function insertChildAt(parentNode, childNode, index) {
|
||||
// We can rely exclusively on `insertBefore(node, null)` instead of also using
|
||||
function insertChildAt(parentNode, childNode, referenceNode) {
|
||||
// We rely exclusively on `insertBefore(node, null)` instead of also using
|
||||
// `appendChild(node)`. (Using `undefined` is not allowed by all browsers so
|
||||
// we are careful to use `null`.)
|
||||
|
||||
// In Safari, .childNodes[index] can return a DOM node with id={index} so we
|
||||
// use .item() instead which is immune to this bug. (See #3560.) In contrast
|
||||
// to the spec, IE8 throws an error if index is larger than the list size.
|
||||
var referenceNode =
|
||||
index < parentNode.childNodes.length ?
|
||||
parentNode.childNodes.item(index) : null;
|
||||
|
||||
parentNode.insertBefore(childNode, referenceNode);
|
||||
}
|
||||
|
||||
function insertLazyTreeChildAt(parentNode, childTree, index) {
|
||||
// See above.
|
||||
var referenceNode =
|
||||
index < parentNode.childNodes.length ?
|
||||
parentNode.childNodes.item(index) : null;
|
||||
|
||||
function insertLazyTreeChildAt(parentNode, childTree, referenceNode) {
|
||||
DOMLazyTree.insertTreeBefore(parentNode, childTree, referenceNode);
|
||||
}
|
||||
|
||||
@@ -69,81 +59,21 @@ var DOMChildrenOperations = {
|
||||
* @internal
|
||||
*/
|
||||
processUpdates: function(parentNode, updates) {
|
||||
var update;
|
||||
// Mapping from parent IDs to initial child orderings.
|
||||
var initialChildren = null;
|
||||
// List of children that will be moved or removed.
|
||||
var updatedChildren = null;
|
||||
|
||||
var markupList = null;
|
||||
|
||||
for (var i = 0; i < updates.length; i++) {
|
||||
update = updates[i];
|
||||
if (update.type === ReactMultiChildUpdateTypes.MOVE_EXISTING ||
|
||||
update.type === ReactMultiChildUpdateTypes.REMOVE_NODE) {
|
||||
var updatedIndex = update.fromIndex;
|
||||
var updatedChild = parentNode.childNodes[updatedIndex];
|
||||
|
||||
invariant(
|
||||
updatedChild,
|
||||
'processUpdates(): Unable to find child %s of element %s. This ' +
|
||||
'probably means the DOM was unexpectedly mutated (e.g., by the ' +
|
||||
'browser), usually due to forgetting a <tbody> when using tables, ' +
|
||||
'nesting tags like <form>, <p>, or <a>, or using non-SVG elements ' +
|
||||
'in an <svg> parent.',
|
||||
updatedIndex,
|
||||
parentNode,
|
||||
);
|
||||
|
||||
initialChildren = initialChildren || {};
|
||||
initialChildren[updatedIndex] = updatedChild;
|
||||
|
||||
updatedChildren = updatedChildren || [];
|
||||
updatedChildren.push(updatedChild);
|
||||
} else if (update.type === ReactMultiChildUpdateTypes.INSERT_MARKUP) {
|
||||
// Replace each HTML string with an index into the markup list
|
||||
if (typeof update.content === 'string') {
|
||||
markupList = markupList || [];
|
||||
update.content = markupList.push(update.markup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var renderedMarkup;
|
||||
if (markupList) {
|
||||
renderedMarkup = Danger.dangerouslyRenderMarkup(markupList);
|
||||
}
|
||||
|
||||
// Remove updated children first so that `toIndex` is consistent.
|
||||
if (updatedChildren) {
|
||||
for (var j = 0; j < updatedChildren.length; j++) {
|
||||
parentNode.removeChild(updatedChildren[j]);
|
||||
}
|
||||
}
|
||||
|
||||
for (var k = 0; k < updates.length; k++) {
|
||||
update = updates[k];
|
||||
var update = updates[k];
|
||||
switch (update.type) {
|
||||
case ReactMultiChildUpdateTypes.INSERT_MARKUP:
|
||||
if (renderedMarkup) {
|
||||
insertChildAt(
|
||||
parentNode,
|
||||
renderedMarkup[update.content],
|
||||
update.toIndex
|
||||
);
|
||||
} else {
|
||||
insertLazyTreeChildAt(
|
||||
parentNode,
|
||||
update.content,
|
||||
update.toIndex
|
||||
);
|
||||
}
|
||||
insertLazyTreeChildAt(
|
||||
parentNode,
|
||||
update.content,
|
||||
getNodeAfter(parentNode, update.afterNode)
|
||||
);
|
||||
break;
|
||||
case ReactMultiChildUpdateTypes.MOVE_EXISTING:
|
||||
insertChildAt(
|
||||
parentNode,
|
||||
initialChildren[update.fromIndex],
|
||||
update.toIndex
|
||||
update.fromNode,
|
||||
getNodeAfter(parentNode, update.afterNode)
|
||||
);
|
||||
break;
|
||||
case ReactMultiChildUpdateTypes.SET_MARKUP:
|
||||
@@ -159,7 +89,7 @@ var DOMChildrenOperations = {
|
||||
);
|
||||
break;
|
||||
case ReactMultiChildUpdateTypes.REMOVE_NODE:
|
||||
// Already removed by the for-loop above.
|
||||
parentNode.removeChild(update.fromNode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ var ReactChildReconciler = {
|
||||
updateChildren: function(
|
||||
prevChildren,
|
||||
nextChildren,
|
||||
removedNodes,
|
||||
transaction,
|
||||
context) {
|
||||
// We currently don't have a way to track moves here but if we use iterators
|
||||
@@ -79,14 +80,15 @@ var ReactChildReconciler = {
|
||||
// TODO: If nothing has changed, return the prevChildren object so that we
|
||||
// can quickly bailout if nothing has changed.
|
||||
if (!nextChildren && !prevChildren) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
var name;
|
||||
var prevChild;
|
||||
for (name in nextChildren) {
|
||||
if (!nextChildren.hasOwnProperty(name)) {
|
||||
continue;
|
||||
}
|
||||
var prevChild = prevChildren && prevChildren[name];
|
||||
prevChild = prevChildren && prevChildren[name];
|
||||
var prevElement = prevChild && prevChild._currentElement;
|
||||
var nextElement = nextChildren[name];
|
||||
if (prevChild != null &&
|
||||
@@ -97,7 +99,8 @@ var ReactChildReconciler = {
|
||||
nextChildren[name] = prevChild;
|
||||
} else {
|
||||
if (prevChild) {
|
||||
ReactReconciler.unmountComponent(prevChild, name);
|
||||
removedNodes[name] = ReactReconciler.getNativeNode(prevChild);
|
||||
ReactReconciler.unmountComponent(prevChild);
|
||||
}
|
||||
// The child must be instantiated before it's mounted.
|
||||
var nextChildInstance = instantiateReactComponent(nextElement);
|
||||
@@ -108,10 +111,11 @@ var ReactChildReconciler = {
|
||||
for (name in prevChildren) {
|
||||
if (prevChildren.hasOwnProperty(name) &&
|
||||
!(nextChildren && nextChildren.hasOwnProperty(name))) {
|
||||
ReactReconciler.unmountComponent(prevChildren[name]);
|
||||
prevChild = prevChildren[name];
|
||||
removedNodes[name] = ReactReconciler.getNativeNode(prevChild);
|
||||
ReactReconciler.unmountComponent(prevChild);
|
||||
}
|
||||
}
|
||||
return nextChildren;
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,6 +19,7 @@ var ReactReconciler = require('ReactReconciler');
|
||||
var ReactChildReconciler = require('ReactChildReconciler');
|
||||
|
||||
var flattenChildren = require('flattenChildren');
|
||||
var invariant = require('invariant');
|
||||
|
||||
/**
|
||||
* Make an update for markup to be rendered and inserted at a supplied index.
|
||||
@@ -27,13 +28,15 @@ var flattenChildren = require('flattenChildren');
|
||||
* @param {number} toIndex Destination index.
|
||||
* @private
|
||||
*/
|
||||
function makeInsertMarkup(markup, toIndex) {
|
||||
function makeInsertMarkup(markup, afterNode, toIndex) {
|
||||
// NOTE: Null values reduce hidden classes.
|
||||
return {
|
||||
type: ReactMultiChildUpdateTypes.INSERT_MARKUP,
|
||||
content: markup,
|
||||
fromIndex: null,
|
||||
fromNode: null,
|
||||
toIndex: toIndex,
|
||||
afterNode: afterNode,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,13 +47,15 @@ function makeInsertMarkup(markup, toIndex) {
|
||||
* @param {number} toIndex Destination index of the element.
|
||||
* @private
|
||||
*/
|
||||
function makeMove(fromIndex, toIndex) {
|
||||
function makeMove(child, afterNode, toIndex) {
|
||||
// NOTE: Null values reduce hidden classes.
|
||||
return {
|
||||
type: ReactMultiChildUpdateTypes.MOVE_EXISTING,
|
||||
content: null,
|
||||
fromIndex: fromIndex,
|
||||
fromIndex: child._mountIndex,
|
||||
fromNode: ReactReconciler.getNativeNode(child),
|
||||
toIndex: toIndex,
|
||||
afterNode: afterNode,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -60,13 +65,15 @@ function makeMove(fromIndex, toIndex) {
|
||||
* @param {number} fromIndex Index of the element to remove.
|
||||
* @private
|
||||
*/
|
||||
function makeRemove(fromIndex) {
|
||||
function makeRemove(child, node) {
|
||||
// NOTE: Null values reduce hidden classes.
|
||||
return {
|
||||
type: ReactMultiChildUpdateTypes.REMOVE_NODE,
|
||||
content: null,
|
||||
fromIndex: fromIndex,
|
||||
fromIndex: child._mountIndex,
|
||||
fromNode: node,
|
||||
toIndex: null,
|
||||
afterNode: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,7 +89,9 @@ function makeSetMarkup(markup) {
|
||||
type: ReactMultiChildUpdateTypes.SET_MARKUP,
|
||||
content: markup,
|
||||
fromIndex: null,
|
||||
fromNode: null,
|
||||
toIndex: null,
|
||||
afterNode: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -98,7 +107,9 @@ function makeTextContent(textContent) {
|
||||
type: ReactMultiChildUpdateTypes.TEXT_CONTENT,
|
||||
content: textContent,
|
||||
fromIndex: null,
|
||||
fromNode: null,
|
||||
toIndex: null,
|
||||
afterNode: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -161,7 +172,13 @@ var ReactMultiChild = {
|
||||
);
|
||||
},
|
||||
|
||||
_reconcilerUpdateChildren: function(prevChildren, nextNestedChildrenElements, transaction, context) {
|
||||
_reconcilerUpdateChildren: function(
|
||||
prevChildren,
|
||||
nextNestedChildrenElements,
|
||||
removedNodes,
|
||||
transaction,
|
||||
context
|
||||
) {
|
||||
var nextChildren;
|
||||
if (__DEV__) {
|
||||
if (this._currentElement) {
|
||||
@@ -171,15 +188,17 @@ var ReactMultiChild = {
|
||||
} finally {
|
||||
ReactCurrentOwner.current = null;
|
||||
}
|
||||
return ReactChildReconciler.updateChildren(
|
||||
prevChildren, nextChildren, transaction, context
|
||||
ReactChildReconciler.updateChildren(
|
||||
prevChildren, nextChildren, removedNodes, transaction, context
|
||||
);
|
||||
return nextChildren;
|
||||
}
|
||||
}
|
||||
nextChildren = flattenChildren(nextNestedChildrenElements);
|
||||
return ReactChildReconciler.updateChildren(
|
||||
prevChildren, nextChildren, transaction, context
|
||||
ReactChildReconciler.updateChildren(
|
||||
prevChildren, nextChildren, removedNodes, transaction, context
|
||||
);
|
||||
return nextChildren;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -224,15 +243,13 @@ var ReactMultiChild = {
|
||||
var prevChildren = this._renderedChildren;
|
||||
// Remove any rendered children.
|
||||
ReactChildReconciler.unmountChildren(prevChildren);
|
||||
// TODO: The setTextContent operation should be enough
|
||||
var updates = [];
|
||||
for (var name in prevChildren) {
|
||||
if (prevChildren.hasOwnProperty(name)) {
|
||||
updates.push(this._unmountChild(prevChildren[name]));
|
||||
invariant(false, 'updateTextContent called on non-empty component.');
|
||||
}
|
||||
}
|
||||
// Set new text content.
|
||||
updates.push(makeTextContent(nextContent));
|
||||
var updates = [makeTextContent(nextContent)];
|
||||
processQueue(this, updates);
|
||||
},
|
||||
|
||||
@@ -246,13 +263,12 @@ var ReactMultiChild = {
|
||||
var prevChildren = this._renderedChildren;
|
||||
// Remove any rendered children.
|
||||
ReactChildReconciler.unmountChildren(prevChildren);
|
||||
var updates = [];
|
||||
for (var name in prevChildren) {
|
||||
if (prevChildren.hasOwnProperty(name)) {
|
||||
updates.push(this._unmountChild(prevChildren[name]));
|
||||
invariant(false, 'updateTextContent called on non-empty component.');
|
||||
}
|
||||
}
|
||||
updates.push(makeSetMarkup(nextMarkup));
|
||||
var updates = [makeSetMarkup(nextMarkup)];
|
||||
processQueue(this, updates);
|
||||
},
|
||||
|
||||
@@ -276,8 +292,13 @@ var ReactMultiChild = {
|
||||
*/
|
||||
_updateChildren: function(nextNestedChildrenElements, transaction, context) {
|
||||
var prevChildren = this._renderedChildren;
|
||||
var removedNodes = {};
|
||||
var nextChildren = this._reconcilerUpdateChildren(
|
||||
prevChildren, nextNestedChildrenElements, transaction, context
|
||||
prevChildren,
|
||||
nextNestedChildrenElements,
|
||||
removedNodes,
|
||||
transaction,
|
||||
context
|
||||
);
|
||||
if (!nextChildren && !prevChildren) {
|
||||
return;
|
||||
@@ -288,6 +309,7 @@ var ReactMultiChild = {
|
||||
// `lastIndex` will be the last index visited in `prevChildren`.
|
||||
var lastIndex = 0;
|
||||
var nextIndex = 0;
|
||||
var lastPlacedNode = null;
|
||||
for (name in nextChildren) {
|
||||
if (!nextChildren.hasOwnProperty(name)) {
|
||||
continue;
|
||||
@@ -297,7 +319,7 @@ var ReactMultiChild = {
|
||||
if (prevChild === nextChild) {
|
||||
updates = enqueue(
|
||||
updates,
|
||||
this.moveChild(prevChild, nextIndex, lastIndex)
|
||||
this.moveChild(prevChild, lastPlacedNode, nextIndex, lastIndex)
|
||||
);
|
||||
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
|
||||
prevChild._mountIndex = nextIndex;
|
||||
@@ -305,23 +327,29 @@ var ReactMultiChild = {
|
||||
if (prevChild) {
|
||||
// Update `lastIndex` before `_mountIndex` gets unset by unmounting.
|
||||
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
|
||||
updates = enqueue(updates, this._unmountChild(prevChild));
|
||||
// The `removedNodes` loop below will actually remove the child.
|
||||
}
|
||||
// The child must be instantiated before it's mounted.
|
||||
updates = enqueue(
|
||||
updates,
|
||||
this._mountChildAtIndex(nextChild, nextIndex, transaction, context)
|
||||
this._mountChildAtIndex(
|
||||
nextChild,
|
||||
lastPlacedNode,
|
||||
nextIndex,
|
||||
transaction,
|
||||
context
|
||||
)
|
||||
);
|
||||
}
|
||||
nextIndex++;
|
||||
lastPlacedNode = ReactReconciler.getNativeNode(nextChild);
|
||||
}
|
||||
// Remove children that are no longer present.
|
||||
for (name in prevChildren) {
|
||||
if (prevChildren.hasOwnProperty(name) &&
|
||||
!(nextChildren && nextChildren.hasOwnProperty(name))) {
|
||||
for (name in removedNodes) {
|
||||
if (removedNodes.hasOwnProperty(name)) {
|
||||
updates = enqueue(
|
||||
updates,
|
||||
this._unmountChild(prevChildren[name])
|
||||
this._unmountChild(prevChildren[name], removedNodes[name])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -334,7 +362,8 @@ var ReactMultiChild = {
|
||||
|
||||
/**
|
||||
* Unmounts all rendered children. This should be used to clean up children
|
||||
* when this component is unmounted.
|
||||
* when this component is unmounted. It does not actually perform any
|
||||
* backend operations.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
@@ -360,12 +389,12 @@ var ReactMultiChild = {
|
||||
* @param {number} lastIndex Last index visited of the siblings of `child`.
|
||||
* @protected
|
||||
*/
|
||||
moveChild: function(child, toIndex, lastIndex) {
|
||||
moveChild: function(child, afterNode, toIndex, lastIndex) {
|
||||
// If the index of `child` is less than `lastIndex`, then it needs to
|
||||
// be moved. Otherwise, we do not need to move it because a child will be
|
||||
// inserted or moved before `child`.
|
||||
if (child._mountIndex < lastIndex) {
|
||||
return makeMove(child._mountIndex, toIndex);
|
||||
return makeMove(child, afterNode, toIndex);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -376,8 +405,8 @@ var ReactMultiChild = {
|
||||
* @param {string} mountImage Markup to insert.
|
||||
* @protected
|
||||
*/
|
||||
createChild: function(child, mountImage) {
|
||||
return makeInsertMarkup(mountImage, child._mountIndex);
|
||||
createChild: function(child, afterNode, mountImage) {
|
||||
return makeInsertMarkup(mountImage, afterNode, child._mountIndex);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -386,8 +415,8 @@ var ReactMultiChild = {
|
||||
* @param {ReactComponent} child Child to remove.
|
||||
* @protected
|
||||
*/
|
||||
removeChild: function(child) {
|
||||
return makeRemove(child._mountIndex);
|
||||
removeChild: function(child, node) {
|
||||
return makeRemove(child, node);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -403,6 +432,7 @@ var ReactMultiChild = {
|
||||
*/
|
||||
_mountChildAtIndex: function(
|
||||
child,
|
||||
afterNode,
|
||||
index,
|
||||
transaction,
|
||||
context) {
|
||||
@@ -414,7 +444,7 @@ var ReactMultiChild = {
|
||||
context
|
||||
);
|
||||
child._mountIndex = index;
|
||||
return this.createChild(child, mountImage);
|
||||
return this.createChild(child, afterNode, mountImage);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -425,8 +455,8 @@ var ReactMultiChild = {
|
||||
* @param {ReactComponent} child Component to unmount.
|
||||
* @private
|
||||
*/
|
||||
_unmountChild: function(child) {
|
||||
var update = this.removeChild(child);
|
||||
_unmountChild: function(child, node) {
|
||||
var update = this.removeChild(child, node);
|
||||
child._mountIndex = null;
|
||||
return update;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user