Skip to content

Commit

Permalink
Merge pull request #5205 from spicyj/cotree-num
Browse files Browse the repository at this point in the history
Use incrementing numerical IDs to identify DOM components
  • Loading branch information
sophiebits committed Nov 4, 2015
2 parents 84af306 + c193d1a commit 35962a0
Show file tree
Hide file tree
Showing 68 changed files with 1,808 additions and 2,291 deletions.
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

0 comments on commit 35962a0

Please sign in to comment.