Skip to content
188 changes: 188 additions & 0 deletions src/renderers/dom/client/ReactDOMComponentTree.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/**
* 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 = childInst._rootNodeID;
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) === 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 causes the next
// invariant for a missing parent, which is super confusing.
invariant('_nativeNode' in inst, '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
134 changes: 134 additions & 0 deletions src/renderers/dom/client/ReactDOMTreeTraversal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* Copyright 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 ReactDOMTreeTraversal
*/

'use strict';

var invariant = require('invariant');

/**
* Return the lowest common ancestor of A and B, or null if they are in
* different trees.
*/
function getLowestCommonAncestor(instA, instB) {
invariant('_nativeNode' in instA, 'getNodeFromInstance: Invalid argument.');
invariant('_nativeNode' in instB, 'getNodeFromInstance: Invalid argument.');

var depthA = 0;
for (var tempA = instA; tempA; tempA = tempA._nativeParent) {
depthA++;
}
var depthB = 0;
for (var tempB = instB; tempB; tempB = tempB._nativeParent) {
depthB++;
}

// If A is deeper, crawl up.
while (depthA - depthB > 0) {
instA = instA._nativeParent;
depthA--;
}

// If B is deeper, crawl up.
while (depthB - depthA > 0) {
instB = instB._nativeParent;
depthB--;
}

// Walk in lockstep until we find a match.
var depth = depthA;
while (depth--) {
if (instA === instB) {
return instA;
}
instA = instA._nativeParent;
instB = instB._nativeParent;
}
return null;
}

/**
* Return if A is an ancestor of B.
*/
function isAncestor(instA, instB) {
invariant('_nativeNode' in instA, 'isAncestor: Invalid argument.');
invariant('_nativeNode' in instB, 'isAncestor: Invalid argument.');

while (instB) {
if (instB === instA) {
return true;
}
instB = instB._nativeParent;
}
return false;
}

/**
* Return the parent instance of the passed-in instance.
*/
function getParentInstance(inst) {
invariant('_nativeNode' in inst, 'getParentInstance: Invalid argument.');

return inst._nativeParent;
}

/**
* Simulates the traversal of a two-phase, capture/bubble event dispatch.
*/
function traverseTwoPhase(inst, fn, arg) {
var path = [];
while (inst) {
path.push(inst);
inst = inst._nativeParent;
}
var i;
for (i = path.length; i-- > 0;) {
fn(path[i], false, arg);
}
for (i = 0; i < path.length; i++) {
fn(path[i], true, arg);
}
}

/**
* Traverses the ID hierarchy and invokes the supplied `cb` on any IDs that
* should would receive a `mouseEnter` or `mouseLeave` event.
*
* Does not invoke the callback on the nearest common ancestor because nothing
* "entered" or "left" that element.
*/
function traverseEnterLeave(from, to, fn, argFrom, argTo) {
var common = from && to ? getLowestCommonAncestor(from, to) : null;
var pathFrom = [];
while (from && from !== common) {
pathFrom.push(from);
from = from._nativeParent;
}
var pathTo = [];
while (to && to !== common) {
pathTo.push(to);
to = to._nativeParent;
}
var i;
for (i = 0; i < pathFrom.length; i++) {
fn(pathFrom[i], true, argFrom);
}
for (i = pathTo.length; i-- > 0;) {
fn(pathTo[i], false, argTo);
}
}

module.exports = {
isAncestor: isAncestor,
getLowestCommonAncestor: getLowestCommonAncestor,
getParentInstance: getParentInstance,
traverseTwoPhase: traverseTwoPhase,
traverseEnterLeave: traverseEnterLeave,
};
Loading