Skip to content

Use incrementing numerical IDs to identify DOM components #5205

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Nov 4, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions src/isomorphic/children/__tests__/ReactChildren-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,16 +240,16 @@ describe('ReactChildren', function() {
mappedChildren[2].key,
mappedChildren[3].key,
]).toEqual([
'giraffe/.0:$firstHalfKey/=1$keyZero',
'/.0:$firstHalfKey/=1$keyTwo',
'keyFour/.0:$secondHalfKey/=1$keyFour',
'/.0:$keyFive/=1$keyFiveInner',
'giraffe/.0:$firstHalfKey/.$keyZero',
'/.0:$firstHalfKey/.$keyTwo',
'keyFour/.0:$secondHalfKey/.$keyFour',
'/.0:$keyFive/.$keyFiveInner',
]);

expect(mappedChildren[0]).toEqual(<div key="giraffe/.0:$firstHalfKey/=1$keyZero" />);
expect(mappedChildren[1]).toEqual(<div key="/.0:$firstHalfKey/=1$keyTwo" />);
expect(mappedChildren[2]).toEqual(<div key="keyFour/.0:$secondHalfKey/=1$keyFour" />);
expect(mappedChildren[3]).toEqual(<div key="/.0:$keyFive/=1$keyFiveInner" />);
expect(mappedChildren[0]).toEqual(<div key="giraffe/.0:$firstHalfKey/.$keyZero" />);
expect(mappedChildren[1]).toEqual(<div key="/.0:$firstHalfKey/.$keyTwo" />);
expect(mappedChildren[2]).toEqual(<div key="keyFour/.0:$secondHalfKey/.$keyFour" />);
expect(mappedChildren[3]).toEqual(<div key="/.0:$keyFive/.$keyFiveInner" />);
});

it('should retain key across two mappings', function() {
Expand Down Expand Up @@ -279,8 +279,8 @@ describe('ReactChildren', function() {
expect(mappedForcedKeys).toEqual(expectedForcedKeys);

var expectedRemappedForcedKeys = [
'giraffe/.$giraffe/=1$keyZero',
'/.$/=1$keyOne',
'giraffe/.$giraffe/.$keyZero',
'/.$/.$keyOne',
];
var remappedChildrenForcedKeys =
ReactChildren.map(mappedChildrenForcedKeys, mapFn);
Expand Down
23 changes: 17 additions & 6 deletions src/renderers/dom/ReactDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,16 @@

'use strict';

var ReactCurrentOwner = require('ReactCurrentOwner');
var ReactDOMTextComponent = require('ReactDOMTextComponent');
var ReactDOMComponentTree = require('ReactDOMComponentTree');
var ReactDefaultInjection = require('ReactDefaultInjection');
var ReactInstanceHandles = require('ReactInstanceHandles');
var ReactMount = require('ReactMount');
var ReactPerf = require('ReactPerf');
var ReactReconciler = require('ReactReconciler');
var ReactUpdates = require('ReactUpdates');
var ReactVersion = require('ReactVersion');

var findDOMNode = require('findDOMNode');
var getNativeComponentFromComposite = require('getNativeComponentFromComposite');
var renderSubtreeIntoContainer = require('renderSubtreeIntoContainer');
var warning = require('warning');

Expand All @@ -49,11 +48,23 @@ if (
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' &&
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.inject === 'function') {
__REACT_DEVTOOLS_GLOBAL_HOOK__.inject({
CurrentOwner: ReactCurrentOwner,
InstanceHandles: ReactInstanceHandles,
ComponentTree: {
getClosestInstanceFromNode:
ReactDOMComponentTree.getClosestInstanceFromNode,
getNodeFromInstance: function(inst) {
// inst is an internal instance (but could be a composite)
if (inst._renderedComponent) {
inst = getNativeComponentFromComposite(inst);
}
if (inst) {
return ReactDOMComponentTree.getNodeFromInstance(inst);
} else {
return null;
}
},
},
Mount: ReactMount,
Reconciler: ReactReconciler,
TextComponent: ReactDOMTextComponent,
});
}

Expand Down
191 changes: 191 additions & 0 deletions src/renderers/dom/client/ReactDOMComponentTree.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactDOMComponentTree
*/

'use strict';

var DOMProperty = require('DOMProperty');
var ReactDOMComponentFlags = require('ReactDOMComponentFlags');

var invariant = require('invariant');

var ATTR_NAME = DOMProperty.ID_ATTRIBUTE_NAME;
var Flags = ReactDOMComponentFlags;

var internalInstanceKey =
'__reactInternalInstance$' + Math.random().toString(36).slice(2);

/**
* Drill down (through composites and empty components) until we get a native or
* native text component.
*
* This is pretty polymorphic but unavoidable with the current structure we have
* for `_renderedChildren`.
*/
function getRenderedNativeOrTextFromComponent(component) {
var rendered;
while ((rendered = component._renderedComponent)) {
component = rendered;
}
return component;
}

/**
* Populate `_nativeNode` on the rendered native/text component with the given
* DOM node. The passed `inst` can be a composite.
*/
function precacheNode(inst, node) {
var nativeInst = getRenderedNativeOrTextFromComponent(inst);
nativeInst._nativeNode = node;
node[internalInstanceKey] = nativeInst;
}

function uncacheNode(inst) {
var node = inst._nativeNode;
if (node) {
delete node[internalInstanceKey];
inst._nativeNode = null;
}
}

/**
* Populate `_nativeNode` on each child of `inst`, assuming that the children
* match up with the DOM (element) children of `node`.
*
* We cache entire levels at once to avoid an n^2 problem where we access the
* children of a node sequentially and have to walk from the start to our target
* node every time.
*
* Since we update `_renderedChildren` and the actual DOM at (slightly)
* different times, we could race here and not get the
*/
function precacheChildNodes(inst, node) {
if (inst._flags & Flags.hasCachedChildNodes) {
return;
}
var children = inst._renderedChildren;
var childNode = node.firstChild;
outer: for (var name in children) {
if (!children.hasOwnProperty(name)) {
continue;
}
var childInst = children[name];
var childID = getRenderedNativeOrTextFromComponent(childInst)._domID;
if (childID == null) {
// We're currently unmounting this child in ReactMultiChild; skip it.
continue;
}
// We assume the child nodes are in the same order as the child instances.
for (; childNode !== null; childNode = childNode.nextSibling) {
if (childNode.nodeType === 1 &&
childNode.getAttribute(ATTR_NAME) === String(childID)) {
precacheNode(childInst, childNode);
continue outer;
}
}
// We reached the end of the DOM children without finding an ID match.
invariant(false, 'Unable to find element with ID %s.', childID);
}
inst._flags |= Flags.hasCachedChildNodes;
}

/**
* Given a DOM node, return the closest ReactDOMComponent or
* ReactDOMTextComponent instance ancestor.
*/
function getClosestInstanceFromNode(node) {
if (node[internalInstanceKey]) {
return node[internalInstanceKey];
}

// Walk up the tree until we find an ancestor whose instance we have cached.
var parents = [];
while (!node[internalInstanceKey]) {
parents.push(node);
if (node.parentNode) {
node = node.parentNode;
} else {
// Top of the tree. This node must not be part of a React tree (or is
// unmounted, potentially).
return null;
}
}

var closest;
var inst;
for (; node && (inst = node[internalInstanceKey]); node = parents.pop()) {
closest = inst;
if (parents.length) {
precacheChildNodes(inst, node);
}
}

return closest;
}

/**
* Given a DOM node, return the ReactDOMComponent or ReactDOMTextComponent
* instance, or null if the node was not rendered by this React.
*/
function getInstanceFromNode(node) {
var inst = getClosestInstanceFromNode(node);
if (inst != null && inst._nativeNode === node) {
return inst;
} else {
return null;
}
}

/**
* Given a ReactDOMComponent or ReactDOMTextComponent, return the corresponding
* DOM node.
*/
function getNodeFromInstance(inst) {
// Without this first invariant, passing a non-DOM-component triggers the next
// invariant for a missing parent, which is super confusing.
invariant(
inst._nativeNode !== undefined,
'getNodeFromInstance: Invalid argument.'
);

if (inst._nativeNode) {
return inst._nativeNode;
}

// Walk up the tree until we find an ancestor whose DOM node we have cached.
var parents = [];
while (!inst._nativeNode) {
parents.push(inst);
invariant(
inst._nativeParent,
'React DOM tree root should always have a node reference.'
);
inst = inst._nativeParent;
}

// Now parents contains each ancestor that does *not* have a cached native
// node, and `inst` is the deepest ancestor that does.
for (; parents.length; inst = parents.pop()) {
precacheChildNodes(inst, inst._nativeNode);
}

return inst._nativeNode;
}

var ReactDOMComponentTree = {
getClosestInstanceFromNode: getClosestInstanceFromNode,
getInstanceFromNode: getInstanceFromNode,
getNodeFromInstance: getNodeFromInstance,
precacheChildNodes: precacheChildNodes,
precacheNode: precacheNode,
uncacheNode: uncacheNode,
};

module.exports = ReactDOMComponentTree;
48 changes: 4 additions & 44 deletions src/renderers/dom/client/ReactDOMIDOperations.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,56 +13,14 @@
'use strict';

var DOMChildrenOperations = require('DOMChildrenOperations');
var DOMPropertyOperations = require('DOMPropertyOperations');
var ReactMount = require('ReactMount');
var ReactDOMComponentTree = require('ReactDOMComponentTree');
var ReactPerf = require('ReactPerf');

var invariant = require('invariant');

/**
* Errors for properties that should not be updated with `updatePropertyByID()`.
*
* @type {object}
* @private
*/
var INVALID_PROPERTY_ERRORS = {
dangerouslySetInnerHTML:
'`dangerouslySetInnerHTML` must be set using `updateInnerHTMLByID()`.',
style: '`style` must be set using `updateStylesByID()`.',
};

/**
* Operations used to process updates to DOM nodes.
*/
var ReactDOMIDOperations = {

/**
* Updates a DOM node with new property values. This should only be used to
* update DOM properties in `DOMProperty`.
*
* @param {string} id ID of the node to update.
* @param {string} name A valid property name, see `DOMProperty`.
* @param {*} value New value of the property.
* @internal
*/
updatePropertyByID: function(id, name, value) {
var node = ReactMount.getNode(id);
invariant(
!INVALID_PROPERTY_ERRORS.hasOwnProperty(name),
'updatePropertyByID(...): %s',
INVALID_PROPERTY_ERRORS[name]
);

// If we're updating to null or undefined, we should remove the property
// from the DOM node instead of inadvertantly setting to a string. This
// brings us in line with the same behavior we have on initial render.
if (value != null) {
DOMPropertyOperations.setValueForProperty(node, name, value);
} else {
DOMPropertyOperations.deleteValueForProperty(node, name);
}
},

/**
* Updates a component's children by processing a series of updates.
*
Expand All @@ -72,7 +30,9 @@ var ReactDOMIDOperations = {
*/
dangerouslyProcessChildrenUpdates: function(updates, markup) {
for (var i = 0; i < updates.length; i++) {
updates[i].parentNode = ReactMount.getNode(updates[i].parentID);
var update = updates[i];
var node = ReactDOMComponentTree.getNodeFromInstance(update.parentInst);
update.parentNode = node;
}
DOMChildrenOperations.processUpdates(updates, markup);
},
Expand Down
Loading