mirror of
https://github.com/zebrajr/react.git
synced 2025-12-07 12:20:38 +01:00
440 lines
14 KiB
JavaScript
440 lines
14 KiB
JavaScript
/**
|
|
* Copyright 2013-2014 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 ReactDOMComponent
|
|
* @typechecks static-only
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
var CSSPropertyOperations = require('CSSPropertyOperations');
|
|
var DOMProperty = require('DOMProperty');
|
|
var DOMPropertyOperations = require('DOMPropertyOperations');
|
|
var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin');
|
|
var ReactComponent = require('ReactComponent');
|
|
var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
|
|
var ReactMount = require('ReactMount');
|
|
var ReactMultiChild = require('ReactMultiChild');
|
|
var ReactPerf = require('ReactPerf');
|
|
|
|
var escapeTextForBrowser = require('escapeTextForBrowser');
|
|
var invariant = require('invariant');
|
|
var isEventSupported = require('isEventSupported');
|
|
var keyOf = require('keyOf');
|
|
var merge = require('merge');
|
|
var mixInto = require('mixInto');
|
|
var monitorCodeUse = require('monitorCodeUse');
|
|
|
|
var deleteListener = ReactBrowserEventEmitter.deleteListener;
|
|
var listenTo = ReactBrowserEventEmitter.listenTo;
|
|
var registrationNameModules = ReactBrowserEventEmitter.registrationNameModules;
|
|
|
|
// For quickly matching children type, to test if can be treated as content.
|
|
var CONTENT_TYPES = {'string': true, 'number': true};
|
|
|
|
var STYLE = keyOf({style: null});
|
|
|
|
var ELEMENT_NODE_TYPE = 1;
|
|
|
|
/**
|
|
* @param {?object} props
|
|
*/
|
|
function assertValidProps(props) {
|
|
if (!props) {
|
|
return;
|
|
}
|
|
// Note the use of `==` which checks for null or undefined.
|
|
invariant(
|
|
props.children == null || props.dangerouslySetInnerHTML == null,
|
|
'Can only set one of `children` or `props.dangerouslySetInnerHTML`.'
|
|
);
|
|
if (__DEV__) {
|
|
if (props.contentEditable && props.children != null) {
|
|
console.warn(
|
|
'A component is `contentEditable` and contains `children` managed by ' +
|
|
'React. It is now your responsibility to guarantee that none of those '+
|
|
'nodes are unexpectedly modified or duplicated. This is probably not ' +
|
|
'intentional.'
|
|
);
|
|
}
|
|
}
|
|
invariant(
|
|
props.style == null || typeof props.style === 'object',
|
|
'The `style` prop expects a mapping from style properties to values, ' +
|
|
'not a string.'
|
|
);
|
|
}
|
|
|
|
function putListener(id, registrationName, listener, transaction) {
|
|
if (__DEV__) {
|
|
// IE8 has no API for event capturing and the `onScroll` event doesn't
|
|
// bubble.
|
|
if (registrationName === 'onScroll' &&
|
|
!isEventSupported('scroll', true)) {
|
|
monitorCodeUse('react_no_scroll_event');
|
|
console.warn('This browser doesn\'t support the `onScroll` event');
|
|
}
|
|
}
|
|
var container = ReactMount.findReactContainerForID(id);
|
|
if (container) {
|
|
var doc = container.nodeType === ELEMENT_NODE_TYPE ?
|
|
container.ownerDocument :
|
|
container;
|
|
listenTo(registrationName, doc);
|
|
}
|
|
transaction.getPutListenerQueue().enqueuePutListener(
|
|
id,
|
|
registrationName,
|
|
listener
|
|
);
|
|
}
|
|
|
|
|
|
/**
|
|
* @constructor ReactDOMComponent
|
|
* @extends ReactComponent
|
|
* @extends ReactMultiChild
|
|
*/
|
|
function ReactDOMComponent(tag, omitClose) {
|
|
this._tagOpen = '<' + tag;
|
|
this._tagClose = omitClose ? '' : '</' + tag + '>';
|
|
this.tagName = tag.toUpperCase();
|
|
}
|
|
|
|
ReactDOMComponent.Mixin = {
|
|
|
|
/**
|
|
* Generates root tag markup then recurses. This method has side effects and
|
|
* is not idempotent.
|
|
*
|
|
* @internal
|
|
* @param {string} rootID The root DOM ID for this node.
|
|
* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
|
|
* @param {number} mountDepth number of components in the owner hierarchy
|
|
* @return {string} The computed markup.
|
|
*/
|
|
mountComponent: ReactPerf.measure(
|
|
'ReactDOMComponent',
|
|
'mountComponent',
|
|
function(rootID, transaction, mountDepth) {
|
|
ReactComponent.Mixin.mountComponent.call(
|
|
this,
|
|
rootID,
|
|
transaction,
|
|
mountDepth
|
|
);
|
|
assertValidProps(this.props);
|
|
return (
|
|
this._createOpenTagMarkupAndPutListeners(transaction) +
|
|
this._createContentMarkup(transaction) +
|
|
this._tagClose
|
|
);
|
|
}
|
|
),
|
|
|
|
/**
|
|
* Creates markup for the open tag and all attributes.
|
|
*
|
|
* This method has side effects because events get registered.
|
|
*
|
|
* Iterating over object properties is faster than iterating over arrays.
|
|
* @see http://jsperf.com/obj-vs-arr-iteration
|
|
*
|
|
* @private
|
|
* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
|
|
* @return {string} Markup of opening tag.
|
|
*/
|
|
_createOpenTagMarkupAndPutListeners: function(transaction) {
|
|
var props = this.props;
|
|
var ret = this._tagOpen;
|
|
|
|
for (var propKey in props) {
|
|
if (!props.hasOwnProperty(propKey)) {
|
|
continue;
|
|
}
|
|
var propValue = props[propKey];
|
|
if (propValue == null) {
|
|
continue;
|
|
}
|
|
if (registrationNameModules.hasOwnProperty(propKey)) {
|
|
putListener(this._rootNodeID, propKey, propValue, transaction);
|
|
} else {
|
|
if (propKey === STYLE) {
|
|
if (propValue) {
|
|
propValue = props.style = merge(props.style);
|
|
}
|
|
propValue = CSSPropertyOperations.createMarkupForStyles(propValue);
|
|
}
|
|
var markup =
|
|
DOMPropertyOperations.createMarkupForProperty(propKey, propValue);
|
|
if (markup) {
|
|
ret += ' ' + markup;
|
|
}
|
|
}
|
|
}
|
|
|
|
// For static pages, no need to put React ID and checksum. Saves lots of
|
|
// bytes.
|
|
if (transaction.renderToStaticMarkup) {
|
|
return ret + '>';
|
|
}
|
|
|
|
var markupForID = DOMPropertyOperations.createMarkupForID(this._rootNodeID);
|
|
return ret + ' ' + markupForID + '>';
|
|
},
|
|
|
|
/**
|
|
* Creates markup for the content between the tags.
|
|
*
|
|
* @private
|
|
* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
|
|
* @return {string} Content markup.
|
|
*/
|
|
_createContentMarkup: function(transaction) {
|
|
// Intentional use of != to avoid catching zero/false.
|
|
var innerHTML = this.props.dangerouslySetInnerHTML;
|
|
if (innerHTML != null) {
|
|
if (innerHTML.__html != null) {
|
|
return innerHTML.__html;
|
|
}
|
|
} else {
|
|
var contentToUse =
|
|
CONTENT_TYPES[typeof this.props.children] ? this.props.children : null;
|
|
var childrenToUse = contentToUse != null ? null : this.props.children;
|
|
if (contentToUse != null) {
|
|
return escapeTextForBrowser(contentToUse);
|
|
} else if (childrenToUse != null) {
|
|
var mountImages = this.mountChildren(
|
|
childrenToUse,
|
|
transaction
|
|
);
|
|
return mountImages.join('');
|
|
}
|
|
}
|
|
return '';
|
|
},
|
|
|
|
receiveComponent: function(nextDescriptor, transaction) {
|
|
if (nextDescriptor === this._descriptor &&
|
|
nextDescriptor._owner != null) {
|
|
// Since descriptors are immutable after the owner is rendered,
|
|
// we can do a cheap identity compare here to determine if this is a
|
|
// superfluous reconcile. It's possible for state to be mutable but such
|
|
// change should trigger an update of the owner which would recreate
|
|
// the descriptor. We explicitly check for the existence of an owner since
|
|
// it's possible for a descriptor created outside a composite to be
|
|
// deeply mutated and reused.
|
|
return;
|
|
}
|
|
|
|
ReactComponent.Mixin.receiveComponent.call(
|
|
this,
|
|
nextDescriptor,
|
|
transaction
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Updates a native DOM component after it has already been allocated and
|
|
* attached to the DOM. Reconciles the root DOM node, then recurses.
|
|
*
|
|
* @param {ReactReconcileTransaction} transaction
|
|
* @param {ReactDescriptor} prevDescriptor
|
|
* @internal
|
|
* @overridable
|
|
*/
|
|
updateComponent: ReactPerf.measure(
|
|
'ReactDOMComponent',
|
|
'updateComponent',
|
|
function(transaction, prevDescriptor) {
|
|
assertValidProps(this._descriptor.props);
|
|
ReactComponent.Mixin.updateComponent.call(
|
|
this,
|
|
transaction,
|
|
prevDescriptor
|
|
);
|
|
this._updateDOMProperties(prevDescriptor.props, transaction);
|
|
this._updateDOMChildren(prevDescriptor.props, transaction);
|
|
}
|
|
),
|
|
|
|
/**
|
|
* Reconciles the properties by detecting differences in property values and
|
|
* updating the DOM as necessary. This function is probably the single most
|
|
* critical path for performance optimization.
|
|
*
|
|
* TODO: Benchmark whether checking for changed values in memory actually
|
|
* improves performance (especially statically positioned elements).
|
|
* TODO: Benchmark the effects of putting this at the top since 99% of props
|
|
* do not change for a given reconciliation.
|
|
* TODO: Benchmark areas that can be improved with caching.
|
|
*
|
|
* @private
|
|
* @param {object} lastProps
|
|
* @param {ReactReconcileTransaction} transaction
|
|
*/
|
|
_updateDOMProperties: function(lastProps, transaction) {
|
|
var nextProps = this.props;
|
|
var propKey;
|
|
var styleName;
|
|
var styleUpdates;
|
|
for (propKey in lastProps) {
|
|
if (nextProps.hasOwnProperty(propKey) ||
|
|
!lastProps.hasOwnProperty(propKey)) {
|
|
continue;
|
|
}
|
|
if (propKey === STYLE) {
|
|
var lastStyle = lastProps[propKey];
|
|
for (styleName in lastStyle) {
|
|
if (lastStyle.hasOwnProperty(styleName)) {
|
|
styleUpdates = styleUpdates || {};
|
|
styleUpdates[styleName] = '';
|
|
}
|
|
}
|
|
} else if (registrationNameModules.hasOwnProperty(propKey)) {
|
|
deleteListener(this._rootNodeID, propKey);
|
|
} else if (
|
|
DOMProperty.isStandardName[propKey] ||
|
|
DOMProperty.isCustomAttribute(propKey)) {
|
|
ReactComponent.BackendIDOperations.deletePropertyByID(
|
|
this._rootNodeID,
|
|
propKey
|
|
);
|
|
}
|
|
}
|
|
for (propKey in nextProps) {
|
|
var nextProp = nextProps[propKey];
|
|
var lastProp = lastProps[propKey];
|
|
if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp) {
|
|
continue;
|
|
}
|
|
if (propKey === STYLE) {
|
|
if (nextProp) {
|
|
nextProp = nextProps.style = merge(nextProp);
|
|
}
|
|
if (lastProp) {
|
|
// Unset styles on `lastProp` but not on `nextProp`.
|
|
for (styleName in lastProp) {
|
|
if (lastProp.hasOwnProperty(styleName) &&
|
|
(!nextProp || !nextProp.hasOwnProperty(styleName))) {
|
|
styleUpdates = styleUpdates || {};
|
|
styleUpdates[styleName] = '';
|
|
}
|
|
}
|
|
// Update styles that changed since `lastProp`.
|
|
for (styleName in nextProp) {
|
|
if (nextProp.hasOwnProperty(styleName) &&
|
|
lastProp[styleName] !== nextProp[styleName]) {
|
|
styleUpdates = styleUpdates || {};
|
|
styleUpdates[styleName] = nextProp[styleName];
|
|
}
|
|
}
|
|
} else {
|
|
// Relies on `updateStylesByID` not mutating `styleUpdates`.
|
|
styleUpdates = nextProp;
|
|
}
|
|
} else if (registrationNameModules.hasOwnProperty(propKey)) {
|
|
putListener(this._rootNodeID, propKey, nextProp, transaction);
|
|
} else if (
|
|
DOMProperty.isStandardName[propKey] ||
|
|
DOMProperty.isCustomAttribute(propKey)) {
|
|
ReactComponent.BackendIDOperations.updatePropertyByID(
|
|
this._rootNodeID,
|
|
propKey,
|
|
nextProp
|
|
);
|
|
}
|
|
}
|
|
if (styleUpdates) {
|
|
ReactComponent.BackendIDOperations.updateStylesByID(
|
|
this._rootNodeID,
|
|
styleUpdates
|
|
);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Reconciles the children with the various properties that affect the
|
|
* children content.
|
|
*
|
|
* @param {object} lastProps
|
|
* @param {ReactReconcileTransaction} transaction
|
|
*/
|
|
_updateDOMChildren: function(lastProps, transaction) {
|
|
var nextProps = this.props;
|
|
|
|
var lastContent =
|
|
CONTENT_TYPES[typeof lastProps.children] ? lastProps.children : null;
|
|
var nextContent =
|
|
CONTENT_TYPES[typeof nextProps.children] ? nextProps.children : null;
|
|
|
|
var lastHtml =
|
|
lastProps.dangerouslySetInnerHTML &&
|
|
lastProps.dangerouslySetInnerHTML.__html;
|
|
var nextHtml =
|
|
nextProps.dangerouslySetInnerHTML &&
|
|
nextProps.dangerouslySetInnerHTML.__html;
|
|
|
|
// Note the use of `!=` which checks for null or undefined.
|
|
var lastChildren = lastContent != null ? null : lastProps.children;
|
|
var nextChildren = nextContent != null ? null : nextProps.children;
|
|
|
|
// If we're switching from children to content/html or vice versa, remove
|
|
// the old content
|
|
var lastHasContentOrHtml = lastContent != null || lastHtml != null;
|
|
var nextHasContentOrHtml = nextContent != null || nextHtml != null;
|
|
if (lastChildren != null && nextChildren == null) {
|
|
this.updateChildren(null, transaction);
|
|
} else if (lastHasContentOrHtml && !nextHasContentOrHtml) {
|
|
this.updateTextContent('');
|
|
}
|
|
|
|
if (nextContent != null) {
|
|
if (lastContent !== nextContent) {
|
|
this.updateTextContent('' + nextContent);
|
|
}
|
|
} else if (nextHtml != null) {
|
|
if (lastHtml !== nextHtml) {
|
|
ReactComponent.BackendIDOperations.updateInnerHTMLByID(
|
|
this._rootNodeID,
|
|
nextHtml
|
|
);
|
|
}
|
|
} else if (nextChildren != null) {
|
|
this.updateChildren(nextChildren, transaction);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Destroys all event registrations for this instance. Does not remove from
|
|
* the DOM. That must be done by the parent.
|
|
*
|
|
* @internal
|
|
*/
|
|
unmountComponent: function() {
|
|
this.unmountChildren();
|
|
ReactBrowserEventEmitter.deleteAllListeners(this._rootNodeID);
|
|
ReactComponent.Mixin.unmountComponent.call(this);
|
|
}
|
|
|
|
};
|
|
|
|
mixInto(ReactDOMComponent, ReactComponent.Mixin);
|
|
mixInto(ReactDOMComponent, ReactDOMComponent.Mixin);
|
|
mixInto(ReactDOMComponent, ReactMultiChild.Mixin);
|
|
mixInto(ReactDOMComponent, ReactBrowserComponentMixin);
|
|
|
|
module.exports = ReactDOMComponent;
|