mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
524 lines
16 KiB
JavaScript
524 lines
16 KiB
JavaScript
/**
|
|
* Copyright 2013 Facebook, Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*
|
|
* @providesModule ReactComponent
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
var ReactComponentEnvironment = require('ReactComponentEnvironment');
|
|
var ReactCurrentOwner = require('ReactCurrentOwner');
|
|
var ReactOwner = require('ReactOwner');
|
|
var ReactUpdates = require('ReactUpdates');
|
|
|
|
var invariant = require('invariant');
|
|
var keyMirror = require('keyMirror');
|
|
var merge = require('merge');
|
|
|
|
/**
|
|
* Every React component is in one of these life cycles.
|
|
*/
|
|
var ComponentLifeCycle = keyMirror({
|
|
/**
|
|
* Mounted components have a DOM node representation and are capable of
|
|
* receiving new props.
|
|
*/
|
|
MOUNTED: null,
|
|
/**
|
|
* Unmounted components are inactive and cannot receive new props.
|
|
*/
|
|
UNMOUNTED: null
|
|
});
|
|
|
|
/**
|
|
* Warn if there's no key explicitly set on dynamic arrays of children.
|
|
* This allows us to keep track of children between updates.
|
|
*/
|
|
|
|
var ownerHasWarned = {};
|
|
|
|
/**
|
|
* Warn if the component doesn't have an explicit key assigned to it.
|
|
* This component is in an array. The array could grow and shrink or be
|
|
* reordered. All children, that hasn't already been validated, are required to
|
|
* have a "key" property assigned to it.
|
|
*
|
|
* @internal
|
|
* @param {ReactComponent} component Component that requires a key.
|
|
*/
|
|
function validateExplicitKey(component) {
|
|
if (component.__keyValidated__ || component.props.key != null) {
|
|
return;
|
|
}
|
|
component.__keyValidated__ = true;
|
|
|
|
// We can't provide friendly warnings for top level components.
|
|
if (!ReactCurrentOwner.current) {
|
|
return;
|
|
}
|
|
|
|
// Name of the component whose render method tried to pass children.
|
|
var currentName = ReactCurrentOwner.current.constructor.displayName;
|
|
if (ownerHasWarned.hasOwnProperty(currentName)) {
|
|
return;
|
|
}
|
|
ownerHasWarned[currentName] = true;
|
|
|
|
var message = 'Each child in an array should have a unique "key" prop. ' +
|
|
'Check the render method of ' + currentName + '.';
|
|
if (!component.isOwnedBy(ReactCurrentOwner.current)) {
|
|
// Name of the component that originally created this child.
|
|
var childOwnerName =
|
|
component._owner &&
|
|
component._owner.constructor.displayName;
|
|
|
|
// Usually the current owner is the offender, but if it accepts
|
|
// children as a property, it may be the creator of the child that's
|
|
// responsible for assigning it a key.
|
|
message += ' It was passed a child from ' + childOwnerName + '.';
|
|
}
|
|
|
|
console.warn(message);
|
|
}
|
|
|
|
/**
|
|
* Ensure that every component either is passed in a static location or, if
|
|
* if it's passed in an array, has an explicit key property defined.
|
|
*
|
|
* @internal
|
|
* @param {*} component Statically passed child of any type.
|
|
* @return {boolean}
|
|
*/
|
|
function validateChildKeys(component) {
|
|
if (Array.isArray(component)) {
|
|
for (var i = 0; i < component.length; i++) {
|
|
var child = component[i];
|
|
if (ReactComponent.isValidComponent(child)) {
|
|
validateExplicitKey(child);
|
|
}
|
|
}
|
|
} else if (ReactComponent.isValidComponent(component)) {
|
|
// This component was passed in a valid location.
|
|
component.__keyValidated__ = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Components are the basic units of composition in React.
|
|
*
|
|
* Every component accepts a set of keyed input parameters known as "props" that
|
|
* are initialized by the constructor. Once a component is mounted, the props
|
|
* can be mutated using `setProps` or `replaceProps`.
|
|
*
|
|
* Every component is capable of the following operations:
|
|
*
|
|
* `mountComponent`
|
|
* Initializes the component, renders markup, and registers event listeners.
|
|
*
|
|
* `receiveComponent`
|
|
* Updates the rendered DOM nodes to match the given component.
|
|
*
|
|
* `unmountComponent`
|
|
* Releases any resources allocated by this component.
|
|
*
|
|
* Components can also be "owned" by other components. Being owned by another
|
|
* component means being constructed by that component. This is different from
|
|
* being the child of a component, which means having a DOM representation that
|
|
* is a child of the DOM representation of that component.
|
|
*
|
|
* @class ReactComponent
|
|
*/
|
|
var ReactComponent = {
|
|
|
|
/**
|
|
* @param {?object} object
|
|
* @return {boolean} True if `object` is a valid component.
|
|
* @final
|
|
*/
|
|
isValidComponent: function(object) {
|
|
return !!(
|
|
object &&
|
|
typeof object.mountComponentIntoNode === 'function' &&
|
|
typeof object.receiveComponent === 'function'
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Generate a key string that identifies a component within a set.
|
|
*
|
|
* @param {*} component A component that could contain a manual key.
|
|
* @param {number} index Index that is used if a manual key is not provided.
|
|
* @return {string}
|
|
* @internal
|
|
*/
|
|
getKey: function(component, index) {
|
|
if (component && component.props && component.props.key != null) {
|
|
// Explicit key
|
|
return '{' + component.props.key + '}';
|
|
}
|
|
// Implicit key determined by the index in the set
|
|
return '[' + index + ']';
|
|
},
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
LifeCycle: ComponentLifeCycle,
|
|
|
|
/**
|
|
* Injected module that provides ability to mutate individual properties.
|
|
* Injected into the base class because many different subclasses need access
|
|
* to this.
|
|
*
|
|
* @internal
|
|
*/
|
|
DOMIDOperations: ReactComponentEnvironment.DOMIDOperations,
|
|
|
|
/**
|
|
* Optionally injectable environment dependent cleanup hook. (server vs.
|
|
* browser etc). Example: A browser system caches DOM nodes based on component
|
|
* ID and must remove that cache entry when this instance is unmounted.
|
|
*
|
|
* @private
|
|
*/
|
|
unmountIDFromEnvironment: ReactComponentEnvironment.unmountIDFromEnvironment,
|
|
|
|
/**
|
|
* The "image" of a component tree, is the platform specific (typically
|
|
* serialized) data that represents a tree of lower level UI building blocks.
|
|
* On the web, this "image" is HTML markup which describes a construction of
|
|
* low level `div` and `span` nodes. Other platforms may have different
|
|
* encoding of this "image". This must be injected.
|
|
*
|
|
* @private
|
|
*/
|
|
mountImageIntoNode: ReactComponentEnvironment.mountImageIntoNode,
|
|
|
|
/**
|
|
* React references `ReactReconcileTransaction` using this property in order
|
|
* to allow dependency injection.
|
|
*
|
|
* @internal
|
|
*/
|
|
ReactReconcileTransaction:
|
|
ReactComponentEnvironment.ReactReconcileTransaction,
|
|
|
|
/**
|
|
* Base functionality for every ReactComponent constructor. Mixed into the
|
|
* `ReactComponent` prototype, but exposed statically for easy access.
|
|
*
|
|
* @lends {ReactComponent.prototype}
|
|
*/
|
|
Mixin: merge(ReactComponentEnvironment.Mixin, {
|
|
|
|
/**
|
|
* Checks whether or not this component is mounted.
|
|
*
|
|
* @return {boolean} True if mounted, false otherwise.
|
|
* @final
|
|
* @protected
|
|
*/
|
|
isMounted: function() {
|
|
return this._lifeCycleState === ComponentLifeCycle.MOUNTED;
|
|
},
|
|
|
|
/**
|
|
* Sets a subset of the props.
|
|
*
|
|
* @param {object} partialProps Subset of the next props.
|
|
* @param {?function} callback Called after props are updated.
|
|
* @final
|
|
* @public
|
|
*/
|
|
setProps: function(partialProps, callback) {
|
|
// Merge with `_pendingProps` if it exists, otherwise with existing props.
|
|
this.replaceProps(
|
|
merge(this._pendingProps || this.props, partialProps),
|
|
callback
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Replaces all of the props.
|
|
*
|
|
* @param {object} props New props.
|
|
* @param {?function} callback Called after props are updated.
|
|
* @final
|
|
* @public
|
|
*/
|
|
replaceProps: function(props, callback) {
|
|
invariant(
|
|
!this._owner,
|
|
'replaceProps(...): You called `setProps` or `replaceProps` on a ' +
|
|
'component with an owner. This is an anti-pattern since props will ' +
|
|
'get reactively updated when rendered. Instead, change the owner\'s ' +
|
|
'`render` method to pass the correct value as props to the component ' +
|
|
'where it is created.'
|
|
);
|
|
invariant(
|
|
this.isMounted(),
|
|
'replaceProps(...): Can only update a mounted component.'
|
|
);
|
|
this._pendingProps = props;
|
|
ReactUpdates.enqueueUpdate(this, callback);
|
|
},
|
|
|
|
/**
|
|
* Base constructor for all React component.
|
|
*
|
|
* Subclasses that override this method should make sure to invoke
|
|
* `ReactComponent.Mixin.construct.call(this, ...)`.
|
|
*
|
|
* @param {?object} initialProps
|
|
* @param {*} children
|
|
* @internal
|
|
*/
|
|
construct: function(initialProps, children) {
|
|
this.props = initialProps || {};
|
|
// Record the component responsible for creating this component.
|
|
this._owner = ReactCurrentOwner.current;
|
|
// All components start unmounted.
|
|
this._lifeCycleState = ComponentLifeCycle.UNMOUNTED;
|
|
|
|
this._pendingProps = null;
|
|
this._pendingCallbacks = null;
|
|
|
|
// Unlike _pendingProps and _pendingCallbacks, we won't use null to
|
|
// indicate that nothing is pending because it's possible for a component
|
|
// to have a null owner. Instead, an owner change is pending when
|
|
// this._owner !== this._pendingOwner.
|
|
this._pendingOwner = this._owner;
|
|
|
|
// Children can be more than one argument
|
|
var childrenLength = arguments.length - 1;
|
|
if (childrenLength === 1) {
|
|
if (__DEV__) {
|
|
validateChildKeys(children);
|
|
}
|
|
this.props.children = children;
|
|
} else if (childrenLength > 1) {
|
|
var childArray = Array(childrenLength);
|
|
for (var i = 0; i < childrenLength; i++) {
|
|
if (__DEV__) {
|
|
validateChildKeys(arguments[i + 1]);
|
|
}
|
|
childArray[i] = arguments[i + 1];
|
|
}
|
|
this.props.children = childArray;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Initializes the component, renders markup, and registers event listeners.
|
|
*
|
|
* NOTE: This does not insert any nodes into the DOM.
|
|
*
|
|
* Subclasses that override this method should make sure to invoke
|
|
* `ReactComponent.Mixin.mountComponent.call(this, ...)`.
|
|
*
|
|
* @param {string} rootID DOM ID of the root node.
|
|
* @param {ReactReconcileTransaction} transaction
|
|
* @param {number} mountDepth number of components in the owner hierarchy.
|
|
* @return {?string} Rendered markup to be inserted into the DOM.
|
|
* @internal
|
|
*/
|
|
mountComponent: function(rootID, transaction, mountDepth) {
|
|
invariant(
|
|
!this.isMounted(),
|
|
'mountComponent(%s, ...): Can only mount an unmounted component.',
|
|
rootID
|
|
);
|
|
var props = this.props;
|
|
if (props.ref != null) {
|
|
ReactOwner.addComponentAsRefTo(this, props.ref, this._owner);
|
|
}
|
|
this._rootNodeID = rootID;
|
|
this._lifeCycleState = ComponentLifeCycle.MOUNTED;
|
|
this._mountDepth = mountDepth;
|
|
// Effectively: return '';
|
|
},
|
|
|
|
/**
|
|
* Releases any resources allocated by `mountComponent`.
|
|
*
|
|
* NOTE: This does not remove any nodes from the DOM.
|
|
*
|
|
* Subclasses that override this method should make sure to invoke
|
|
* `ReactComponent.Mixin.unmountComponent.call(this)`.
|
|
*
|
|
* @internal
|
|
*/
|
|
unmountComponent: function() {
|
|
invariant(
|
|
this.isMounted(),
|
|
'unmountComponent(): Can only unmount a mounted component.'
|
|
);
|
|
var props = this.props;
|
|
if (props.ref != null) {
|
|
ReactOwner.removeComponentAsRefFrom(this, props.ref, this._owner);
|
|
}
|
|
ReactComponent.unmountIDFromEnvironment(this._rootNodeID);
|
|
this._rootNodeID = null;
|
|
this._lifeCycleState = ComponentLifeCycle.UNMOUNTED;
|
|
},
|
|
|
|
/**
|
|
* Given a new instance of this component, updates the rendered DOM nodes
|
|
* as if that instance was rendered instead.
|
|
*
|
|
* Subclasses that override this method should make sure to invoke
|
|
* `ReactComponent.Mixin.receiveComponent.call(this, ...)`.
|
|
*
|
|
* @param {object} nextComponent Next set of properties.
|
|
* @param {ReactReconcileTransaction} transaction
|
|
* @internal
|
|
*/
|
|
receiveComponent: function(nextComponent, transaction) {
|
|
invariant(
|
|
this.isMounted(),
|
|
'receiveComponent(...): Can only update a mounted component.'
|
|
);
|
|
this._pendingOwner = nextComponent._owner;
|
|
this._pendingProps = nextComponent.props;
|
|
this._performUpdateIfNecessary(transaction);
|
|
},
|
|
|
|
/**
|
|
* Call `_performUpdateIfNecessary` within a new transaction.
|
|
*
|
|
* @param {ReactReconcileTransaction} transaction
|
|
* @internal
|
|
*/
|
|
performUpdateIfNecessary: function() {
|
|
var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
|
|
transaction.perform(this._performUpdateIfNecessary, this, transaction);
|
|
ReactComponent.ReactReconcileTransaction.release(transaction);
|
|
},
|
|
|
|
/**
|
|
* If `_pendingProps` is set, update the component.
|
|
*
|
|
* @param {ReactReconcileTransaction} transaction
|
|
* @internal
|
|
*/
|
|
_performUpdateIfNecessary: function(transaction) {
|
|
if (this._pendingProps == null) {
|
|
return;
|
|
}
|
|
var prevProps = this.props;
|
|
var prevOwner = this._owner;
|
|
this.props = this._pendingProps;
|
|
this._owner = this._pendingOwner;
|
|
this._pendingProps = null;
|
|
this.updateComponent(transaction, prevProps, prevOwner);
|
|
},
|
|
|
|
/**
|
|
* Updates the component's currently mounted representation.
|
|
*
|
|
* @param {ReactReconcileTransaction} transaction
|
|
* @param {object} prevProps
|
|
* @internal
|
|
*/
|
|
updateComponent: function(transaction, prevProps, prevOwner) {
|
|
var props = this.props;
|
|
// If either the owner or a `ref` has changed, make sure the newest owner
|
|
// has stored a reference to `this`, and the previous owner (if different)
|
|
// has forgotten the reference to `this`.
|
|
if (this._owner !== prevOwner || props.ref !== prevProps.ref) {
|
|
if (prevProps.ref != null) {
|
|
ReactOwner.removeComponentAsRefFrom(
|
|
this, prevProps.ref, prevOwner
|
|
);
|
|
}
|
|
// Correct, even if the owner is the same, and only the ref has changed.
|
|
if (props.ref != null) {
|
|
ReactOwner.addComponentAsRefTo(this, props.ref, this._owner);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Mounts this component and inserts it into the DOM.
|
|
*
|
|
* @param {string} rootID DOM ID of the root node.
|
|
* @param {DOMElement} container DOM element to mount into.
|
|
* @param {boolean} shouldReuseMarkup If true, do not insert markup
|
|
* @final
|
|
* @internal
|
|
* @see {ReactMount.renderComponent}
|
|
*/
|
|
mountComponentIntoNode: function(rootID, container, shouldReuseMarkup) {
|
|
var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
|
|
transaction.perform(
|
|
this._mountComponentIntoNode,
|
|
this,
|
|
rootID,
|
|
container,
|
|
transaction,
|
|
shouldReuseMarkup
|
|
);
|
|
ReactComponent.ReactReconcileTransaction.release(transaction);
|
|
},
|
|
|
|
/**
|
|
* @param {string} rootID DOM ID of the root node.
|
|
* @param {DOMElement} container DOM element to mount into.
|
|
* @param {ReactReconcileTransaction} transaction
|
|
* @param {boolean} shouldReuseMarkup If true, do not insert markup
|
|
* @final
|
|
* @private
|
|
*/
|
|
_mountComponentIntoNode: function(
|
|
rootID,
|
|
container,
|
|
transaction,
|
|
shouldReuseMarkup) {
|
|
var markup = this.mountComponent(rootID, transaction, 0);
|
|
ReactComponent.mountImageIntoNode(markup, container, shouldReuseMarkup);
|
|
},
|
|
|
|
/**
|
|
* Checks if this component is owned by the supplied `owner` component.
|
|
*
|
|
* @param {ReactComponent} owner Component to check.
|
|
* @return {boolean} True if `owners` owns this component.
|
|
* @final
|
|
* @internal
|
|
*/
|
|
isOwnedBy: function(owner) {
|
|
return this._owner === owner;
|
|
},
|
|
|
|
/**
|
|
* Gets another component, that shares the same owner as this one, by ref.
|
|
*
|
|
* @param {string} ref of a sibling Component.
|
|
* @return {?ReactComponent} the actual sibling Component.
|
|
* @final
|
|
* @internal
|
|
*/
|
|
getSiblingByRef: function(ref) {
|
|
var owner = this._owner;
|
|
if (!owner || !owner.refs) {
|
|
return null;
|
|
}
|
|
return owner.refs[ref];
|
|
}
|
|
})
|
|
};
|
|
|
|
module.exports = ReactComponent;
|