diff --git a/src/core/ReactChildReconciler.js b/src/core/ReactChildReconciler.js new file mode 100644 index 0000000000..209d3f9ad6 --- /dev/null +++ b/src/core/ReactChildReconciler.js @@ -0,0 +1,121 @@ +/** + * Copyright 2014, 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 ReactChildReconciler + * @typechecks static-only + */ + +"use strict"; + +var flattenChildren = require('flattenChildren'); +var instantiateReactComponent = require('instantiateReactComponent'); +var shouldUpdateReactComponent = require('shouldUpdateReactComponent'); + +/** + * ReactChildReconciler provides helpers for initializing or updating a set of + * children. Its output is suitable for passing it onto ReactMultiChild which + * does diffed reordering and insertion. + */ +var ReactChildReconciler = { + + /** + * Generates a "mount image" for each of the supplied children. In the case + * of `ReactDOMComponent`, a mount image is a string of markup. + * + * @param {?object} nestedChildNodes Nested child maps. + * @return {?object} A set of child instances. + * @internal + */ + instantiateChildren: function(nestedChildNodes, transaction, context) { + var children = flattenChildren(nestedChildNodes); + for (var name in children) { + if (children.hasOwnProperty(name)) { + var child = children[name]; + // The rendered children must be turned into instances as they're + // mounted. + var childInstance = instantiateReactComponent(child, null); + children[name] = childInstance; + } + } + return children; + }, + + /** + * Updates the rendered children and returns a new set of children. + * + * @param {?object} prevChildren Previously initialized set of children. + * @param {?object} nextNestedChildNodes Nested child maps. + * @param {ReactReconcileTransaction} transaction + * @param {object} context + * @return {?object} A new set of child instances. + * @internal + */ + updateChildren: function( + prevChildren, + nextNestedChildNodes, + transaction, + context) { + // We currently don't have a way to track moves here but if we use iterators + // instead of for..in we can zip the iterators and check if an item has + // moved. + // TODO: If nothing has changed, return the prevChildren object so that we + // can quickly bailout if nothing has changed. + var nextChildren = flattenChildren(nextNestedChildNodes); + if (!nextChildren && !prevChildren) { + return; + } + var name; + for (name in nextChildren) { + if (!nextChildren.hasOwnProperty(name)) { + continue; + } + var prevChild = prevChildren && prevChildren[name]; + var prevElement = prevChild && prevChild._currentElement; + var nextElement = nextChildren[name]; + if (shouldUpdateReactComponent(prevElement, nextElement)) { + prevChild.receiveComponent(nextElement, transaction, context); + nextChildren[name] = prevChild; + } else { + if (prevChild) { + prevChild.unmountComponent(); + } + // The child must be instantiated before it's mounted. + var nextChildInstance = instantiateReactComponent( + nextElement, + null + ); + nextChildren[name] = nextChildInstance; + } + } + // Unmount children that are no longer present. + for (name in prevChildren) { + if (prevChildren.hasOwnProperty(name) && + !(nextChildren && nextChildren.hasOwnProperty(name))) { + prevChildren[name].unmountComponent(); + } + } + return nextChildren; + }, + + /** + * Unmounts all rendered children. This should be used to clean up children + * when this component is unmounted. + * + * @param {?object} renderedChildren Previously initialized set of children. + * @internal + */ + unmountChildren: function(renderedChildren) { + for (var name in renderedChildren) { + var renderedChild = renderedChildren[name]; + renderedChild.unmountComponent(); + } + } + +}; + +module.exports = ReactChildReconciler; diff --git a/src/core/ReactMultiChild.js b/src/core/ReactMultiChild.js index 15cf15b73f..7960c22ab0 100644 --- a/src/core/ReactMultiChild.js +++ b/src/core/ReactMultiChild.js @@ -15,9 +15,7 @@ var ReactComponentEnvironment = require('ReactComponentEnvironment'); var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes'); -var flattenChildren = require('flattenChildren'); -var instantiateReactComponent = require('instantiateReactComponent'); -var shouldUpdateReactComponent = require('shouldUpdateReactComponent'); +var ReactChildReconciler = require('ReactChildReconciler'); /** * Updating children of a component may trigger recursive updates. The depth is @@ -179,25 +177,23 @@ var ReactMultiChild = { * @internal */ mountChildren: function(nestedChildren, transaction, context) { - var children = flattenChildren(nestedChildren); + var children = ReactChildReconciler.instantiateChildren( + nestedChildren, transaction, context + ); + this._renderedChildren = children; var mountImages = []; var index = 0; - this._renderedChildren = children; for (var name in children) { var child = children[name]; if (children.hasOwnProperty(name)) { - // The rendered children must be turned into instances as they're - // mounted. - var childInstance = instantiateReactComponent(child, null); - children[name] = childInstance; // Inlined for performance, see `ReactInstanceHandles.createReactID`. var rootID = this._rootNodeID + name; - var mountImage = childInstance.mountComponent( + var mountImage = child.mountComponent( rootID, transaction, context ); - childInstance._mountIndex = index; + child._mountIndex = index; mountImages.push(mountImage); index++; } @@ -217,6 +213,8 @@ var ReactMultiChild = { try { var prevChildren = this._renderedChildren; // Remove any rendered children. + ReactChildReconciler.unmountChildren(prevChildren); + // TODO: The setTextContent operation should be enough for (var name in prevChildren) { if (prevChildren.hasOwnProperty(name)) { this._unmountChildByName(prevChildren[name], name); @@ -264,8 +262,11 @@ var ReactMultiChild = { * @protected */ _updateChildren: function(nextNestedChildren, transaction, context) { - var nextChildren = flattenChildren(nextNestedChildren); var prevChildren = this._renderedChildren; + var nextChildren = ReactChildReconciler.updateChildren( + prevChildren, nextNestedChildren, transaction, context + ); + this._renderedChildren = nextChildren; if (!nextChildren && !prevChildren) { return; } @@ -279,12 +280,10 @@ var ReactMultiChild = { continue; } var prevChild = prevChildren && prevChildren[name]; - var prevElement = prevChild && prevChild._currentElement; - var nextElement = nextChildren[name]; - if (shouldUpdateReactComponent(prevElement, nextElement)) { + var nextChild = nextChildren[name]; + if (prevChild === nextChild) { this.moveChild(prevChild, nextIndex, lastIndex); lastIndex = Math.max(prevChild._mountIndex, lastIndex); - prevChild.receiveComponent(nextElement, transaction, context); prevChild._mountIndex = nextIndex; } else { if (prevChild) { @@ -293,12 +292,8 @@ var ReactMultiChild = { this._unmountChildByName(prevChild, name); } // The child must be instantiated before it's mounted. - var nextChildInstance = instantiateReactComponent( - nextElement, - null - ); this._mountChildByNameAtIndex( - nextChildInstance, name, nextIndex, transaction, context + nextChild, name, nextIndex, transaction, context ); } nextIndex++; @@ -320,13 +315,7 @@ var ReactMultiChild = { */ unmountChildren: function() { var renderedChildren = this._renderedChildren; - for (var name in renderedChildren) { - var renderedChild = renderedChildren[name]; - // TODO: When is this not true? - if (renderedChild.unmountComponent) { - renderedChild.unmountComponent(); - } - } + ReactChildReconciler.unmountChildren(renderedChildren); this._renderedChildren = null; }, @@ -404,8 +393,6 @@ var ReactMultiChild = { ); child._mountIndex = index; this.createChild(child, mountImage); - this._renderedChildren = this._renderedChildren || {}; - this._renderedChildren[name] = child; }, /** @@ -420,8 +407,6 @@ var ReactMultiChild = { _unmountChildByName: function(child, name) { this.removeChild(child); child._mountIndex = null; - child.unmountComponent(); - delete this._renderedChildren[name]; } }