From ce528457a49306cf7b0fb4008e9c15aa8d68b93e Mon Sep 17 00:00:00 2001 From: Ben Alpert Date: Thu, 15 Oct 2015 16:09:57 -0700 Subject: [PATCH] ReactDOMComponentTree New module! With this, we have a new module that uses the component tree to store DOM nodes. Most of the logic in this file relates to markup adoption -- if we were to drop that (along with server rendering) this file could just be a `return inst._nativeNode;`. This works with useCreateElement only because we need to reference each node as it's created. Events is now the only thing using ReactMount.getNode -- I'll introduce pointers back from the DOM nodes (and a `ReactDOMComponentTree.getInstanceFromNode`) and make that work. --- .../dom/client/ReactDOMComponentTree.js | 122 ++++++++++++++++++ .../dom/client/ReactDOMIDOperations.js | 48 +------ src/renderers/dom/client/ReactMount.js | 5 + .../__tests__/ReactDOMComponentTree-test.js | 62 +++++++++ .../__tests__/ReactDOMIDOperations-test.js | 32 +---- src/renderers/dom/client/findDOMNode.js | 4 +- .../dom/client/wrappers/AutoFocusUtils.js | 4 +- .../dom/client/wrappers/ReactDOMInput.js | 13 +- .../dom/client/wrappers/ReactDOMSelect.js | 4 +- .../dom/client/wrappers/ReactDOMTextarea.js | 7 +- src/renderers/dom/shared/ReactDOMComponent.js | 39 +++--- .../dom/shared/ReactDOMComponentFlags.js | 19 +++ .../dom/shared/ReactDOMTextComponent.js | 12 +- .../shared/reconciler/ReactMultiChild.js | 53 +++++--- src/test/ReactDefaultPerf.js | 2 +- 15 files changed, 289 insertions(+), 137 deletions(-) create mode 100644 src/renderers/dom/client/ReactDOMComponentTree.js create mode 100644 src/renderers/dom/client/__tests__/ReactDOMComponentTree-test.js create mode 100644 src/renderers/dom/shared/ReactDOMComponentFlags.js diff --git a/src/renderers/dom/client/ReactDOMComponentTree.js b/src/renderers/dom/client/ReactDOMComponentTree.js new file mode 100644 index 0000000000..068c34a5e6 --- /dev/null +++ b/src/renderers/dom/client/ReactDOMComponentTree.js @@ -0,0 +1,122 @@ +/** + * 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; + +/** + * 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; +} + +/** + * 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 ReactDOMComponent or ReactDOMTextComponent, return the corresponding + * DOM node. + */ +function getNodeFromInstance(inst) { + 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 = { + getNodeFromInstance: getNodeFromInstance, + precacheChildNodes: precacheChildNodes, + precacheNode: precacheNode, +}; + +module.exports = ReactDOMComponentTree; diff --git a/src/renderers/dom/client/ReactDOMIDOperations.js b/src/renderers/dom/client/ReactDOMIDOperations.js index 7b419e2bf9..98bd1df70f 100644 --- a/src/renderers/dom/client/ReactDOMIDOperations.js +++ b/src/renderers/dom/client/ReactDOMIDOperations.js @@ -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. * @@ -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); }, diff --git a/src/renderers/dom/client/ReactMount.js b/src/renderers/dom/client/ReactMount.js index dca4ac180a..57af82b39e 100644 --- a/src/renderers/dom/client/ReactMount.js +++ b/src/renderers/dom/client/ReactMount.js @@ -16,6 +16,7 @@ var DOMLazyTree = require('DOMLazyTree'); var DOMProperty = require('DOMProperty'); var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter'); var ReactCurrentOwner = require('ReactCurrentOwner'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactDOMContainerInfo = require('ReactDOMContainerInfo'); var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); var ReactElement = require('ReactElement'); @@ -267,6 +268,7 @@ function mountComponentIntoNode( ReactMount._mountImageIntoNode( markup, container, + componentInstance, shouldReuseMarkup, transaction ); @@ -951,6 +953,7 @@ var ReactMount = { _mountImageIntoNode: function( markup, container, + instance, shouldReuseMarkup, transaction ) { @@ -966,6 +969,7 @@ var ReactMount = { if (shouldReuseMarkup) { var rootElement = getReactRootElementInContainer(container); if (ReactMarkupChecksum.canReuseMarkup(markup, rootElement)) { + ReactDOMComponentTree.precacheNode(instance, rootElement); return; } else { var checksum = rootElement.getAttribute( @@ -1049,6 +1053,7 @@ var ReactMount = { DOMLazyTree.insertTreeBefore(container, markup, null); } else { setInnerHTML(container, markup); + ReactDOMComponentTree.precacheNode(instance, container.firstChild); } }, diff --git a/src/renderers/dom/client/__tests__/ReactDOMComponentTree-test.js b/src/renderers/dom/client/__tests__/ReactDOMComponentTree-test.js new file mode 100644 index 0000000000..7c436199ed --- /dev/null +++ b/src/renderers/dom/client/__tests__/ReactDOMComponentTree-test.js @@ -0,0 +1,62 @@ +/** + * 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. + * + * @emails react-core + */ + +'use strict'; + +describe('ReactDOMComponentTree', function() { + var React; + var ReactDOM; + var ReactDOMServer; + + function renderMarkupIntoDocument(elt) { + var container = document.createElement('div'); + // Force server-rendering path: + container.innerHTML = ReactDOMServer.renderToString(elt); + return ReactDOM.render(elt, container); + } + + beforeEach(function() { + React = require('React'); + ReactDOM = require('ReactDOM'); + ReactDOMServer = require('ReactDOMServer'); + }); + + it('finds nodes for instances', function() { + // This is a little hard to test directly. But refs rely on it -- so we + // check that we can find a ref at arbitrary points in the tree, even if + // other nodes don't have a ref. + var Component = React.createClass({ + render: function() { + var toRef = this.props.toRef; + return ( +
+

hello

+

+ +

+ goodbye. +
+ ); + }, + }); + + function renderAndGetRef(toRef) { + var inst = renderMarkupIntoDocument(); + return inst.refs.target.nodeName; + } + + expect(renderAndGetRef('div')).toBe('DIV'); + expect(renderAndGetRef('h1')).toBe('H1'); + expect(renderAndGetRef('p')).toBe('P'); + expect(renderAndGetRef('input')).toBe('INPUT'); + }); + +}); diff --git a/src/renderers/dom/client/__tests__/ReactDOMIDOperations-test.js b/src/renderers/dom/client/__tests__/ReactDOMIDOperations-test.js index 18eb7378a2..2def47ff20 100644 --- a/src/renderers/dom/client/__tests__/ReactDOMIDOperations-test.js +++ b/src/renderers/dom/client/__tests__/ReactDOMIDOperations-test.js @@ -12,42 +12,16 @@ 'use strict'; describe('ReactDOMIDOperations', function() { - var DOMPropertyOperations = require('DOMPropertyOperations'); var ReactDOMIDOperations = require('ReactDOMIDOperations'); - var ReactMount = require('ReactMount'); var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes'); - var keyOf = require('keyOf'); - - it('should disallow updating special properties', function() { - spyOn(ReactMount, 'getNode'); - spyOn(DOMPropertyOperations, 'setValueForProperty'); - - expect(function() { - ReactDOMIDOperations.updatePropertyByID( - 'testID', - keyOf({dangerouslySetInnerHTML: null}), - {__html: 'testContent'} - ); - }).toThrow(); - - expect( - ReactMount.getNode.argsForCall[0][0] - ).toBe('testID'); - - expect( - DOMPropertyOperations.setValueForProperty.calls.length - ).toBe(0); - }); it('should update innerHTML and preserve whitespace', function() { var stubNode = document.createElement('div'); - spyOn(ReactMount, 'getNode').andReturn(stubNode); - var html = '\n \t \n testContent \t \n \t'; ReactDOMIDOperations.dangerouslyProcessChildrenUpdates( [{ - parentID: 'testID', + parentInst: {_nativeNode: stubNode}, parentNode: null, type: ReactMultiChildUpdateTypes.SET_MARKUP, markupIndex: null, @@ -58,10 +32,6 @@ describe('ReactDOMIDOperations', function() { [] ); - expect( - ReactMount.getNode.argsForCall[0][0] - ).toBe('testID'); - expect(stubNode.innerHTML).toBe(html); }); }); diff --git a/src/renderers/dom/client/findDOMNode.js b/src/renderers/dom/client/findDOMNode.js index 27e8c40ca8..f91ae99fb0 100644 --- a/src/renderers/dom/client/findDOMNode.js +++ b/src/renderers/dom/client/findDOMNode.js @@ -13,7 +13,7 @@ 'use strict'; var ReactCurrentOwner = require('ReactCurrentOwner'); -var ReactDOMComponent = require('ReactDOMComponent'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactInstanceMap = require('ReactInstanceMap'); var ReactNodeTypes = require('ReactNodeTypes'); @@ -66,7 +66,7 @@ function findDOMNode(componentOrElement) { var inst = ReactInstanceMap.get(componentOrElement); if (inst) { inst = getNativeComponentFromComposite(inst); - return inst ? ReactDOMComponent.getNodeFromInstance(inst) : null; + return inst ? ReactDOMComponentTree.getNodeFromInstance(inst) : null; } if (typeof componentOrElement.render === 'function') { diff --git a/src/renderers/dom/client/wrappers/AutoFocusUtils.js b/src/renderers/dom/client/wrappers/AutoFocusUtils.js index 0cc675d3e4..be865e4a82 100644 --- a/src/renderers/dom/client/wrappers/AutoFocusUtils.js +++ b/src/renderers/dom/client/wrappers/AutoFocusUtils.js @@ -12,13 +12,13 @@ 'use strict'; -var ReactMount = require('ReactMount'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); var focusNode = require('focusNode'); var AutoFocusUtils = { focusDOMComponent: function() { - focusNode(ReactMount.getNode(this._rootNodeID)); + focusNode(ReactDOMComponentTree.getNodeFromInstance(this)); }, }; diff --git a/src/renderers/dom/client/wrappers/ReactDOMInput.js b/src/renderers/dom/client/wrappers/ReactDOMInput.js index 0633add1fa..52fce3cdae 100644 --- a/src/renderers/dom/client/wrappers/ReactDOMInput.js +++ b/src/renderers/dom/client/wrappers/ReactDOMInput.js @@ -11,8 +11,9 @@ 'use strict'; -var ReactDOMIDOperations = require('ReactDOMIDOperations'); +var DOMPropertyOperations = require('DOMPropertyOperations'); var LinkedValueUtils = require('LinkedValueUtils'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactMount = require('ReactMount'); var ReactUpdates = require('ReactUpdates'); @@ -131,8 +132,8 @@ var ReactDOMInput = { // TODO: Shouldn't this be getChecked(props)? var checked = props.checked; if (checked != null) { - ReactDOMIDOperations.updatePropertyByID( - inst._rootNodeID, + DOMPropertyOperations.setValueForProperty( + ReactDOMComponentTree.getNodeFromInstance(inst), 'checked', checked || false ); @@ -142,8 +143,8 @@ var ReactDOMInput = { if (value != null) { // Cast `value` to a string to ensure the value is set correctly. While // browsers typically do this as necessary, jsdom doesn't. - ReactDOMIDOperations.updatePropertyByID( - inst._rootNodeID, + DOMPropertyOperations.setValueForProperty( + ReactDOMComponentTree.getNodeFromInstance(inst), 'value', '' + value ); @@ -163,7 +164,7 @@ function _handleChange(event) { var name = props.name; if (props.type === 'radio' && name != null) { - var rootNode = ReactMount.getNode(this._rootNodeID); + var rootNode = ReactDOMComponentTree.getNodeFromInstance(this); var queryRoot = rootNode; while (queryRoot.parentNode) { diff --git a/src/renderers/dom/client/wrappers/ReactDOMSelect.js b/src/renderers/dom/client/wrappers/ReactDOMSelect.js index c8d12eb969..bf36fc19c2 100644 --- a/src/renderers/dom/client/wrappers/ReactDOMSelect.js +++ b/src/renderers/dom/client/wrappers/ReactDOMSelect.js @@ -12,7 +12,7 @@ 'use strict'; var LinkedValueUtils = require('LinkedValueUtils'); -var ReactMount = require('ReactMount'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactUpdates = require('ReactUpdates'); var assign = require('Object.assign'); @@ -112,7 +112,7 @@ function checkSelectPropTypes(inst, props) { */ function updateOptions(inst, multiple, propValue) { var selectedValue, i; - var options = ReactMount.getNode(inst._rootNodeID).options; + var options = ReactDOMComponentTree.getNodeFromInstance(inst).options; if (multiple) { selectedValue = {}; diff --git a/src/renderers/dom/client/wrappers/ReactDOMTextarea.js b/src/renderers/dom/client/wrappers/ReactDOMTextarea.js index 5ed8dbae32..b206c57d6c 100644 --- a/src/renderers/dom/client/wrappers/ReactDOMTextarea.js +++ b/src/renderers/dom/client/wrappers/ReactDOMTextarea.js @@ -11,8 +11,9 @@ 'use strict'; +var DOMPropertyOperations = require('DOMPropertyOperations'); var LinkedValueUtils = require('LinkedValueUtils'); -var ReactDOMIDOperations = require('ReactDOMIDOperations'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactUpdates = require('ReactUpdates'); var assign = require('Object.assign'); @@ -144,8 +145,8 @@ var ReactDOMTextarea = { if (value != null) { // Cast `value` to a string to ensure the value is set correctly. While // browsers typically do this as necessary, jsdom doesn't. - ReactDOMIDOperations.updatePropertyByID( - inst._rootNodeID, + DOMPropertyOperations.setValueForProperty( + ReactDOMComponentTree.getNodeFromInstance(inst), 'value', '' + value ); diff --git a/src/renderers/dom/shared/ReactDOMComponent.js b/src/renderers/dom/shared/ReactDOMComponent.js index 5235c9deeb..e92898e6ee 100644 --- a/src/renderers/dom/shared/ReactDOMComponent.js +++ b/src/renderers/dom/shared/ReactDOMComponent.js @@ -27,6 +27,8 @@ var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter'); var ReactComponentBrowserEnvironment = require('ReactComponentBrowserEnvironment'); var ReactDOMButton = require('ReactDOMButton'); +var ReactDOMComponentFlags = require('ReactDOMComponentFlags'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactDOMInput = require('ReactDOMInput'); var ReactDOMOption = require('ReactDOMOption'); var ReactDOMSelect = require('ReactDOMSelect'); @@ -46,7 +48,9 @@ var shallowEqual = require('shallowEqual'); var validateDOMNesting = require('validateDOMNesting'); var warning = require('warning'); +var Flags = ReactDOMComponentFlags; var deleteListener = EventPluginHub.deleteListener; +var getNode = ReactDOMComponentTree.getNodeFromInstance; var listenTo = ReactBrowserEventEmitter.listenTo; var registrationNameModules = EventPluginRegistry.registrationNameModules; @@ -493,14 +497,6 @@ function isCustomComponent(tagName, props) { return tagName.indexOf('-') >= 0 || props.is != null; } -function getNode(inst) { - if (inst._nativeNode) { - return inst._nativeNode; - } else { - return inst._nativeNode = ReactMount.getNode(inst._rootNodeID); - } -} - /** * Creates a new React class that is idempotent and capable of containing other * React components. It accepts event listeners and DOM properties that are @@ -523,11 +519,12 @@ function ReactDOMComponent(tag) { this._previousStyle = null; this._previousStyleCopy = null; this._nativeNode = null; + this._nativeParent = null; this._rootNodeID = null; this._nativeContainerInfo = null; this._wrapperState = null; this._topLevelWrapper = null; - this._nodeHasLegacyProperties = false; + this._flags = 0; if (__DEV__) { this._ancestorInfo = null; } @@ -561,6 +558,7 @@ ReactDOMComponent.Mixin = { context ) { this._rootNodeID = rootID; + this._nativeParent = nativeParent; this._nativeContainerInfo = nativeContainerInfo; var props = this._currentElement.props; @@ -664,6 +662,7 @@ ReactDOMComponent.Mixin = { ); } this._nativeNode = el; + this._flags |= Flags.hasCachedChildNodes; DOMPropertyOperations.setAttributeForID(el, this._rootNodeID); // Populate node cache ReactMount.getID(el); @@ -907,7 +906,7 @@ ReactDOMComponent.Mixin = { context ); - if (!canDefineProperty && this._nodeHasLegacyProperties) { + if (!canDefineProperty && this._flags & Flags.nodeHasLegacyProperties) { this._nativeNode.props = nextProps; } @@ -1146,7 +1145,7 @@ ReactDOMComponent.Mixin = { break; } - if (this._nodeHasLegacyProperties) { + if (this._flags & Flags.nodeHasLegacyProperties) { this._nativeNode._reactInternalComponent = null; } this._nativeNode = null; @@ -1159,7 +1158,7 @@ ReactDOMComponent.Mixin = { }, getPublicInstance: function() { - if (this._nodeHasLegacyProperties) { + if (this._flags & Flags.nodeHasLegacyProperties) { return this._nativeNode; } else { var node = getNode(this); @@ -1185,7 +1184,7 @@ ReactDOMComponent.Mixin = { node.props = this._currentElement.props; } - this._nodeHasLegacyProperties = true; + this._flags |= Flags.nodeHasLegacyProperties; return node; } }, @@ -1200,11 +1199,15 @@ ReactPerf.measureMethods(ReactDOMComponent, 'ReactDOMComponent', { assign( ReactDOMComponent.prototype, ReactDOMComponent.Mixin, - ReactMultiChild.Mixin + ReactMultiChild.Mixin, + { + prepareToManageChildren: function() { + // Before we add, remove, or reorder the children of a node, make sure + // we have references to all of its children so we don't lose them, even + // if nefarious browser plugins add extra nodes to our tree. + ReactDOMComponentTree.precacheChildNodes(this, getNode(this)); + }, + } ); -assign(ReactDOMComponent, { - getNodeFromInstance: getNode, -}); - module.exports = ReactDOMComponent; diff --git a/src/renderers/dom/shared/ReactDOMComponentFlags.js b/src/renderers/dom/shared/ReactDOMComponentFlags.js new file mode 100644 index 0000000000..2578f90f75 --- /dev/null +++ b/src/renderers/dom/shared/ReactDOMComponentFlags.js @@ -0,0 +1,19 @@ +/** + * 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 ReactDOMComponentFlags + */ + +'use strict'; + +var ReactDOMComponentFlags = { + nodeHasLegacyProperties: 1 << 0, + hasCachedChildNodes: 1 << 1, +}; + +module.exports = ReactDOMComponentFlags; diff --git a/src/renderers/dom/shared/ReactDOMTextComponent.js b/src/renderers/dom/shared/ReactDOMTextComponent.js index 9858379d04..49e9f72291 100644 --- a/src/renderers/dom/shared/ReactDOMTextComponent.js +++ b/src/renderers/dom/shared/ReactDOMTextComponent.js @@ -17,19 +17,14 @@ var DOMLazyTree = require('DOMLazyTree'); var DOMPropertyOperations = require('DOMPropertyOperations'); var ReactComponentBrowserEnvironment = require('ReactComponentBrowserEnvironment'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactMount = require('ReactMount'); var assign = require('Object.assign'); var escapeTextContentForBrowser = require('escapeTextContentForBrowser'); var validateDOMNesting = require('validateDOMNesting'); -function getNode(inst) { - if (inst._nativeNode) { - return inst._nativeNode; - } else { - return inst._nativeNode = ReactMount.getNode(inst._rootNodeID); - } -} +var getNode = ReactDOMComponentTree.getNodeFromInstance; /** * Text nodes violate a couple assumptions that React makes about components: @@ -61,6 +56,8 @@ assign(ReactDOMTextComponent.prototype, { this._currentElement = text; this._stringText = '' + text; this._nativeNode = null; + // ReactDOMComponentTree uses this: + this._nativeParent = null; // Properties this._rootNodeID = null; @@ -98,6 +95,7 @@ assign(ReactDOMTextComponent.prototype, { } this._rootNodeID = rootID; + this._nativeParent = nativeParent; if (transaction.useCreateElement) { var ownerDocument = nativeContainerInfo._ownerDocument; var el = ownerDocument.createElement('span'); diff --git a/src/renderers/shared/reconciler/ReactMultiChild.js b/src/renderers/shared/reconciler/ReactMultiChild.js index 31c073168c..bfb9bda1f4 100644 --- a/src/renderers/shared/reconciler/ReactMultiChild.js +++ b/src/renderers/shared/reconciler/ReactMultiChild.js @@ -51,15 +51,15 @@ var markupQueue = []; /** * Enqueues markup to be rendered and inserted at a supplied index. * - * @param {string} parentID ID of the parent component. + * @param {object} parentInst parent component. * @param {string} markup Markup that renders into an element. * @param {number} toIndex Destination index. * @private */ -function enqueueInsertMarkup(parentID, markup, toIndex) { +function enqueueInsertMarkup(parentInst, markup, toIndex) { // NOTE: Null values reduce hidden classes. updateQueue.push({ - parentID: parentID, + parentInst: parentInst, parentNode: null, type: ReactMultiChildUpdateTypes.INSERT_MARKUP, markupIndex: markupQueue.push(markup) - 1, @@ -72,15 +72,15 @@ function enqueueInsertMarkup(parentID, markup, toIndex) { /** * Enqueues moving an existing element to another index. * - * @param {string} parentID ID of the parent component. + * @param {object} parentInst parent component. * @param {number} fromIndex Source index of the existing element. * @param {number} toIndex Destination index of the element. * @private */ -function enqueueMove(parentID, fromIndex, toIndex) { +function enqueueMove(parentInst, fromIndex, toIndex) { // NOTE: Null values reduce hidden classes. updateQueue.push({ - parentID: parentID, + parentInst: parentInst, parentNode: null, type: ReactMultiChildUpdateTypes.MOVE_EXISTING, markupIndex: null, @@ -93,14 +93,14 @@ function enqueueMove(parentID, fromIndex, toIndex) { /** * Enqueues removing an element at an index. * - * @param {string} parentID ID of the parent component. + * @param {object} parentInst parent component. * @param {number} fromIndex Index of the element to remove. * @private */ -function enqueueRemove(parentID, fromIndex) { +function enqueueRemove(parentInst, fromIndex) { // NOTE: Null values reduce hidden classes. updateQueue.push({ - parentID: parentID, + parentInst: parentInst, parentNode: null, type: ReactMultiChildUpdateTypes.REMOVE_NODE, markupIndex: null, @@ -113,14 +113,14 @@ function enqueueRemove(parentID, fromIndex) { /** * Enqueues setting the markup of a node. * - * @param {string} parentID ID of the parent component. + * @param {object} parentInst parent component. * @param {string} markup Markup that renders into an element. * @private */ -function enqueueSetMarkup(parentID, markup) { +function enqueueSetMarkup(parentInst, markup) { // NOTE: Null values reduce hidden classes. updateQueue.push({ - parentID: parentID, + parentInst: parentInst, parentNode: null, type: ReactMultiChildUpdateTypes.SET_MARKUP, markupIndex: null, @@ -133,14 +133,14 @@ function enqueueSetMarkup(parentID, markup) { /** * Enqueues setting the text content. * - * @param {string} parentID ID of the parent component. + * @param {object} parentInst parent component. * @param {string} textContent Text content to set. * @private */ -function enqueueTextContent(parentID, textContent) { +function enqueueTextContent(parentInst, textContent) { // NOTE: Null values reduce hidden classes. updateQueue.push({ - parentID: parentID, + parentInst: parentInst, parentNode: null, type: ReactMultiChildUpdateTypes.TEXT_CONTENT, markupIndex: null, @@ -371,7 +371,6 @@ var ReactMultiChild = { var nextChildren = this._reconcilerUpdateChildren( prevChildren, nextNestedChildrenElements, transaction, context ); - this._renderedChildren = nextChildren; if (!nextChildren && !prevChildren) { return; } @@ -410,6 +409,7 @@ var ReactMultiChild = { this._unmountChild(prevChildren[name]); } } + this._renderedChildren = nextChildren; }, /** @@ -424,6 +424,14 @@ var ReactMultiChild = { this._renderedChildren = null; }, + /** + * Hook used by the DOM implementation to precache the nodes before we apply + * any reorders here. + */ + prepareToManageChildren: function() { + // TODO: This sucks. Figure out a better design here. + }, + /** * Moves a child component to the supplied index. * @@ -437,7 +445,8 @@ var ReactMultiChild = { // be moved. Otherwise, we do not need to move it because a child will be // inserted or moved before `child`. if (child._mountIndex < lastIndex) { - enqueueMove(this._rootNodeID, child._mountIndex, toIndex); + this.prepareToManageChildren(); + enqueueMove(this, child._mountIndex, toIndex); } }, @@ -449,7 +458,8 @@ var ReactMultiChild = { * @protected */ createChild: function(child, mountImage) { - enqueueInsertMarkup(this._rootNodeID, mountImage, child._mountIndex); + this.prepareToManageChildren(); + enqueueInsertMarkup(this, mountImage, child._mountIndex); }, /** @@ -459,7 +469,8 @@ var ReactMultiChild = { * @protected */ removeChild: function(child) { - enqueueRemove(this._rootNodeID, child._mountIndex); + this.prepareToManageChildren(); + enqueueRemove(this, child._mountIndex); }, /** @@ -469,7 +480,7 @@ var ReactMultiChild = { * @protected */ setTextContent: function(textContent) { - enqueueTextContent(this._rootNodeID, textContent); + enqueueTextContent(this, textContent); }, /** @@ -479,7 +490,7 @@ var ReactMultiChild = { * @protected */ setMarkup: function(markup) { - enqueueSetMarkup(this._rootNodeID, markup); + enqueueSetMarkup(this, markup); }, /** diff --git a/src/test/ReactDefaultPerf.js b/src/test/ReactDefaultPerf.js index 49041637b2..25e76649cf 100644 --- a/src/test/ReactDefaultPerf.js +++ b/src/test/ReactDefaultPerf.js @@ -194,7 +194,7 @@ var ReactDefaultPerf = { writeArgs.markup = args[1][update.markupIndex]; } ReactDefaultPerf._recordWrite( - update.parentID, + update.parentInst._rootNodeID, update.type, totalTime, writeArgs