Use createElement instead of HTML generation

Behind a feature flag. This is a relatively simple change; adopting this strategy universally would mean that we could clean up a lot of code but this doesn't attempt to restructure more than necessary.
This commit is contained in:
Ben Alpert
2015-08-11 20:52:33 -07:00
parent cb2f4de4ca
commit cfd6f7a1b8
19 changed files with 183 additions and 140 deletions

View File

@@ -12,7 +12,6 @@
'use strict';
var CSSPropertyOperations = require('CSSPropertyOperations');
var DOMChildrenOperations = require('DOMChildrenOperations');
var DOMPropertyOperations = require('DOMPropertyOperations');
var ReactMount = require('ReactMount');
@@ -33,8 +32,7 @@ var INVALID_PROPERTY_ERRORS = {
};
/**
* Operations used to process updates to DOM nodes. This is made injectable via
* `ReactDOMComponent.BackendIDOperations`.
* Operations used to process updates to DOM nodes.
*/
var ReactDOMIDOperations = {
@@ -65,67 +63,6 @@ var ReactDOMIDOperations = {
}
},
/**
* Updates a DOM node with new property values.
*
* @param {string} id ID of the node to update.
* @param {string} name A valid property name.
* @param {*} value New value of the property.
* @internal
*/
updateAttributeByID: function(id, name, value) {
var node = ReactMount.getNode(id);
invariant(
!INVALID_PROPERTY_ERRORS.hasOwnProperty(name),
'updatePropertyByID(...): %s',
INVALID_PROPERTY_ERRORS[name]
);
DOMPropertyOperations.setValueForAttribute(node, name, value);
},
/**
* Updates a DOM node to remove a property. This should only be used to remove
* DOM properties in `DOMProperty`.
*
* @param {string} id ID of the node to update.
* @param {string} name A property name to remove, see `DOMProperty`.
* @internal
*/
deletePropertyByID: function(id, name, value) {
var node = ReactMount.getNode(id);
invariant(
!INVALID_PROPERTY_ERRORS.hasOwnProperty(name),
'updatePropertyByID(...): %s',
INVALID_PROPERTY_ERRORS[name]
);
DOMPropertyOperations.deleteValueForProperty(node, name, value);
},
/**
* Updates a DOM node with new style values. If a value is specified as '',
* the corresponding style property will be unset.
*
* @param {string} id ID of the node to update.
* @param {object} styles Mapping from styles to values.
* @internal
*/
updateStylesByID: function(id, styles) {
var node = ReactMount.getNode(id);
CSSPropertyOperations.setValueForStyles(node, styles);
},
/**
* Updates a DOM node's text content set by `props.content`.
*
* @param {string} id ID of the node to update.
* @param {string} content Text content.
* @internal
*/
updateTextContentByID: function(id, content) {
var node = ReactMount.getNode(id);
DOMChildrenOperations.updateTextContent(node, content);
},
/**
* Replaces a DOM node that exists in the document with markup.
*
@@ -156,9 +93,6 @@ var ReactDOMIDOperations = {
ReactPerf.measureMethods(ReactDOMIDOperations, 'ReactDOMIDOperations', {
updatePropertyByID: 'updatePropertyByID',
deletePropertyByID: 'deletePropertyByID',
updateStylesByID: 'updateStylesByID',
updateTextContentByID: 'updateTextContentByID',
dangerouslyReplaceNodeWithMarkupByID: 'dangerouslyReplaceNodeWithMarkupByID',
dangerouslyProcessChildrenUpdates: 'dangerouslyProcessChildrenUpdates',
});

View File

@@ -14,6 +14,7 @@
var DOMProperty = require('DOMProperty');
var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
var ReactCurrentOwner = require('ReactCurrentOwner');
var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
var ReactElement = require('ReactElement');
var ReactEmptyComponent = require('ReactEmptyComponent');
var ReactInstanceHandles = require('ReactInstanceHandles');
@@ -24,6 +25,7 @@ var ReactReconciler = require('ReactReconciler');
var ReactUpdateQueue = require('ReactUpdateQueue');
var ReactUpdates = require('ReactUpdates');
var assign = require('Object.assign');
var emptyObject = require('emptyObject');
var containsNode = require('containsNode');
var instantiateReactComponent = require('instantiateReactComponent');
@@ -42,6 +44,10 @@ var ELEMENT_NODE_TYPE = 1;
var DOC_NODE_TYPE = 9;
var DOCUMENT_FRAGMENT_NODE_TYPE = 11;
var ownerDocumentContextKey =
'__ReactMount_ownerDocument$' + Math.random().toString(36).slice(2);
/** Mapping from reactRootID to React component instance. */
var instancesByReactRootID = {};
@@ -264,6 +270,14 @@ function mountComponentIntoNode(
shouldReuseMarkup,
context
) {
if (ReactDOMFeatureFlags.useCreateElement) {
context = assign({}, context);
if (container.nodeType === DOC_NODE_TYPE) {
context[ownerDocumentContextKey] = container;
} else {
context[ownerDocumentContextKey] = container.ownerDocument;
}
}
if (__DEV__) {
if (context === emptyObject) {
context = {};
@@ -276,7 +290,12 @@ function mountComponentIntoNode(
componentInstance, rootID, transaction, context
);
componentInstance._renderedComponent._topLevelWrapper = componentInstance;
ReactMount._mountImageIntoNode(markup, container, shouldReuseMarkup);
ReactMount._mountImageIntoNode(
markup,
container,
shouldReuseMarkup,
transaction
);
}
/**
@@ -294,7 +313,9 @@ function batchedMountComponentIntoNode(
shouldReuseMarkup,
context
) {
var transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(
/* forceHTML */ shouldReuseMarkup
);
transaction.perform(
mountComponentIntoNode,
null,
@@ -870,7 +891,12 @@ var ReactMount = {
);
},
_mountImageIntoNode: function(markup, container, shouldReuseMarkup) {
_mountImageIntoNode: function(
markup,
container,
shouldReuseMarkup,
transaction
) {
invariant(
container && (
container.nodeType === ELEMENT_NODE_TYPE ||
@@ -959,9 +985,18 @@ var ReactMount = {
'See React.renderToString() for server rendering.'
);
setInnerHTML(container, markup);
if (transaction.useCreateElement) {
while (container.lastChild) {
container.removeChild(container.lastChild);
}
container.appendChild(markup);
} else {
setInnerHTML(container, markup);
}
},
ownerDocumentContextKey: ownerDocumentContextKey,
/**
* React ID utilities.
*/

View File

@@ -15,6 +15,7 @@
var CallbackQueue = require('CallbackQueue');
var PooledClass = require('PooledClass');
var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
var ReactInputSelection = require('ReactInputSelection');
var Transaction = require('Transaction');
@@ -106,7 +107,7 @@ var TRANSACTION_WRAPPERS = [
*
* @class ReactReconcileTransaction
*/
function ReactReconcileTransaction() {
function ReactReconcileTransaction(forceHTML) {
this.reinitializeTransaction();
// Only server-side rendering really needs this option (see
// `ReactServerRendering`), but server-side uses
@@ -115,6 +116,8 @@ function ReactReconcileTransaction() {
// `ReactTextComponent` checks it in `mountComponent`.`
this.renderToStaticMarkup = false;
this.reactMountReady = CallbackQueue.getPooled(null);
this.useCreateElement =
!forceHTML && ReactDOMFeatureFlags.useCreateElement;
}
var Mixin = {

View File

@@ -98,7 +98,13 @@ var DOMChildrenOperations = {
}
}
var renderedMarkup = Danger.dangerouslyRenderMarkup(markupList);
var renderedMarkup;
// markupList is either a list of markup or just a list of elements
if (markupList.length && typeof markupList[0] === 'string') {
renderedMarkup = Danger.dangerouslyRenderMarkup(markupList);
} else {
renderedMarkup = markupList;
}
// Remove updated children first so that `toIndex` is consistent.
if (updatedChildren) {

View File

@@ -62,7 +62,7 @@ describe('ReactDOMTextarea', function() {
});
it('should display "false" for `defaultValue` of `false`', function() {
var stub = <textarea type="text" defaultValue={false} />;
var stub = <textarea defaultValue={false} />;
stub = renderTextarea(stub);
var node = ReactDOM.findDOMNode(stub);
@@ -76,7 +76,7 @@ describe('ReactDOMTextarea', function() {
},
};
var stub = <textarea type="text" defaultValue={objToString} />;
var stub = <textarea defaultValue={objToString} />;
stub = renderTextarea(stub);
var node = ReactDOM.findDOMNode(stub);

View File

@@ -51,6 +51,7 @@ function ReactServerRenderingTransaction(renderToStaticMarkup) {
this.reinitializeTransaction();
this.renderToStaticMarkup = renderToStaticMarkup;
this.reactMountReady = CallbackQueue.getPooled(null);
this.useCreateElement = false;
}
var Mixin = {

View File

@@ -105,6 +105,10 @@ var DOMPropertyOperations = {
quoteAttributeValueForBrowser(id);
},
setAttributeForID: function(node, id) {
node.setAttribute(DOMProperty.ID_ATTRIBUTE_NAME, id);
},
/**
* Creates markup for a property.
*

View File

@@ -172,7 +172,12 @@ var Danger = {
'server rendering. See React.renderToString().'
);
var newChild = createNodesFromMarkup(markup, emptyFunction)[0];
var newChild;
if (typeof markup === 'string') {
newChild = createNodesFromMarkup(markup, emptyFunction)[0];
} else {
newChild = markup;
}
oldChild.parentNode.replaceChild(newChild, oldChild);
},

View File

@@ -37,6 +37,8 @@ var escapeTextContentForBrowser = require('escapeTextContentForBrowser');
var invariant = require('invariant');
var isEventSupported = require('isEventSupported');
var keyOf = require('keyOf');
var setInnerHTML = require('setInnerHTML');
var setTextContent = require('setTextContent');
var shallowEqual = require('shallowEqual');
var validateDOMNesting = require('validateDOMNesting');
var warning = require('warning');
@@ -205,12 +207,6 @@ function checkAndWarnForMutatedStyle(style1, style2, component) {
);
}
/**
* Optionally injectable operations for mutating the DOM
*/
var BackendIDOperations = null;
/**
* @param {object} component
* @param {?object} props
@@ -561,8 +557,26 @@ ReactDOMComponent.Mixin = {
}
}
var tagOpen = this._createOpenTagMarkupAndPutListeners(transaction, props);
var tagContent = this._createContentMarkup(transaction, props, context);
var mountImage;
if (transaction.useCreateElement) {
var ownerDocument = context[ReactMount.ownerDocumentContextKey];
var el = ownerDocument.createElement(this._currentElement.type);
DOMPropertyOperations.setAttributeForID(el, this._rootNodeID);
// Populate node cache
ReactMount.getID(el);
this._updateDOMProperties({}, props, transaction, el);
this._createInitialChildren(transaction, props, context, el);
mountImage = el;
} else {
var tagOpen = this._createOpenTagMarkupAndPutListeners(transaction, props);
var tagContent = this._createContentMarkup(transaction, props, context);
if (!tagContent && omittedCloseTags[this._tag]) {
mountImage = tagOpen + '/>';
} else {
mountImage =
tagOpen + '>' + tagContent + '</' + this._currentElement.type + '>';
}
}
switch (this._tag) {
case 'button':
@@ -578,10 +592,7 @@ ReactDOMComponent.Mixin = {
break;
}
if (!tagContent && omittedCloseTags[this._tag]) {
return tagOpen + '/>';
}
return tagOpen + '>' + tagContent + '</' + this._currentElement.type + '>';
return mountImage;
},
/**
@@ -694,6 +705,33 @@ ReactDOMComponent.Mixin = {
}
},
_createInitialChildren: function(transaction, props, context, el) {
// Intentional use of != to avoid catching zero/false.
var innerHTML = props.dangerouslySetInnerHTML;
if (innerHTML != null) {
if (innerHTML.__html != null) {
setInnerHTML(el, innerHTML.__html);
}
} else {
var contentToUse =
CONTENT_TYPES[typeof props.children] ? props.children : null;
var childrenToUse = contentToUse != null ? null : props.children;
if (contentToUse != null) {
// TODO: Validate that text is allowed as a child of this node
setTextContent(el, contentToUse);
} else if (childrenToUse != null) {
var mountImages = this.mountChildren(
childrenToUse,
transaction,
processChildContext(context, this)
);
for (var i = 0; i < mountImages.length; i++) {
el.appendChild(mountImages[i]);
}
}
}
},
/**
* Receives a next element and updates the component.
*
@@ -747,8 +785,9 @@ ReactDOMComponent.Mixin = {
break;
}
var node = ReactMount.getNode(this._rootNodeID);
assertValidProps(this, nextProps);
this._updateDOMProperties(lastProps, nextProps, transaction);
this._updateDOMProperties(lastProps, nextProps, transaction, node);
this._updateDOMChildren(
lastProps,
nextProps,
@@ -783,7 +822,7 @@ ReactDOMComponent.Mixin = {
* @param {object} nextProps
* @param {ReactReconcileTransaction} transaction
*/
_updateDOMProperties: function(lastProps, nextProps, transaction) {
_updateDOMProperties: function(lastProps, nextProps, transaction, node) {
var propKey;
var styleName;
var styleUpdates;
@@ -811,10 +850,7 @@ ReactDOMComponent.Mixin = {
} else if (
DOMProperty.properties[propKey] ||
DOMProperty.isCustomAttribute(propKey)) {
BackendIDOperations.deletePropertyByID(
this._rootNodeID,
propKey
);
DOMPropertyOperations.deleteValueForProperty(node, propKey);
}
}
for (propKey in nextProps) {
@@ -867,26 +903,26 @@ ReactDOMComponent.Mixin = {
deleteListener(this._rootNodeID, propKey);
}
} else if (isCustomComponent(this._tag, nextProps)) {
BackendIDOperations.updateAttributeByID(
this._rootNodeID,
DOMPropertyOperations.setValueForAttribute(
node,
propKey,
nextProp
);
} else if (
DOMProperty.properties[propKey] ||
DOMProperty.isCustomAttribute(propKey)) {
BackendIDOperations.updatePropertyByID(
this._rootNodeID,
propKey,
nextProp
);
// 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 (nextProp != null) {
DOMPropertyOperations.setValueForProperty(node, propKey, nextProp);
} else {
DOMPropertyOperations.deleteValueForProperty(node, propKey);
}
}
}
if (styleUpdates) {
BackendIDOperations.updateStylesByID(
this._rootNodeID,
styleUpdates
);
CSSPropertyOperations.setValueForStyles(node, styleUpdates);
}
},
@@ -1038,10 +1074,4 @@ assign(
ReactMultiChild.Mixin
);
ReactDOMComponent.injection = {
injectIDOperations: function(IDOperations) {
ReactDOMComponent.BackendIDOperations = BackendIDOperations = IDOperations;
},
};
module.exports = ReactDOMComponent;

View File

@@ -0,0 +1,18 @@
/**
* 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 ReactDOMFeatureFlags
*/
'use strict';
var ReactDOMFeatureFlags = {
useCreateElement: false,
};
module.exports = ReactDOMFeatureFlags;

View File

@@ -12,13 +12,15 @@
'use strict';
var DOMChildrenOperations = require('DOMChildrenOperations');
var DOMPropertyOperations = require('DOMPropertyOperations');
var ReactComponentBrowserEnvironment =
require('ReactComponentBrowserEnvironment');
var ReactDOMComponent = require('ReactDOMComponent');
var ReactMount = require('ReactMount');
var assign = require('Object.assign');
var escapeTextContentForBrowser = require('escapeTextContentForBrowser');
var setTextContent = require('setTextContent');
var validateDOMNesting = require('validateDOMNesting');
/**
@@ -77,20 +79,30 @@ assign(ReactDOMTextComponent.prototype, {
}
this._rootNodeID = rootID;
var escapedText = escapeTextContentForBrowser(this._stringText);
if (transaction.useCreateElement) {
var ownerDocument = context[ReactMount.ownerDocumentContextKey];
var el = ownerDocument.createElement('span');
DOMPropertyOperations.setAttributeForID(el, rootID);
// Populate node cache
ReactMount.getID(el);
setTextContent(el, this._stringText);
return el;
} else {
var escapedText = escapeTextContentForBrowser(this._stringText);
if (transaction.renderToStaticMarkup) {
// Normally we'd wrap this in a `span` for the reasons stated above, but
// since this is a situation where React won't take over (static pages),
// we can simply return the text as it is.
return escapedText;
if (transaction.renderToStaticMarkup) {
// Normally we'd wrap this in a `span` for the reasons stated above, but
// since this is a situation where React won't take over (static pages),
// we can simply return the text as it is.
return escapedText;
}
return (
'<span ' + DOMPropertyOperations.createMarkupForID(rootID) + '>' +
escapedText +
'</span>'
);
}
return (
'<span ' + DOMPropertyOperations.createMarkupForID(rootID) + '>' +
escapedText +
'</span>'
);
},
/**
@@ -109,10 +121,8 @@ assign(ReactDOMTextComponent.prototype, {
// and/or updateComponent to do the actual update for consistency with
// other component types?
this._stringText = nextStringText;
ReactDOMComponent.BackendIDOperations.updateTextContentByID(
this._rootNodeID,
nextStringText
);
var node = ReactMount.getNode(this._rootNodeID);
DOMChildrenOperations.updateTextContent(node, nextStringText);
}
}
},

View File

@@ -23,7 +23,6 @@ var ReactComponentBrowserEnvironment =
require('ReactComponentBrowserEnvironment');
var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy');
var ReactDOMComponent = require('ReactDOMComponent');
var ReactDOMIDOperations = require('ReactDOMIDOperations');
var ReactDOMTextComponent = require('ReactDOMTextComponent');
var ReactEventListener = require('ReactEventListener');
var ReactInjection = require('ReactInjection');
@@ -98,7 +97,6 @@ function inject() {
);
ReactInjection.Component.injectEnvironment(ReactComponentBrowserEnvironment);
ReactInjection.DOMComponent.injectIDOperations(ReactDOMIDOperations);
if (__DEV__) {
var url = (ExecutionEnvironment.canUseDOM && window.location.href) || '';

View File

@@ -18,7 +18,6 @@ var ReactClass = require('ReactClass');
var ReactEmptyComponent = require('ReactEmptyComponent');
var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
var ReactNativeComponent = require('ReactNativeComponent');
var ReactDOMComponent = require('ReactDOMComponent');
var ReactPerf = require('ReactPerf');
var ReactRootIndex = require('ReactRootIndex');
var ReactUpdates = require('ReactUpdates');
@@ -26,7 +25,6 @@ var ReactUpdates = require('ReactUpdates');
var ReactInjection = {
Component: ReactComponentEnvironment.injection,
Class: ReactClass.injection,
DOMComponent: ReactDOMComponent.injection,
DOMProperty: DOMProperty.injection,
EmptyComponent: ReactEmptyComponent.injection,
EventPluginHub: EventPluginHub.injection,

View File

@@ -13,6 +13,7 @@
var React = require('React');
var ReactDOM = require('ReactDOM');
var ReactDOMServer = require('ReactDOMServer');
describe('CSSPropertyOperations', function() {
var CSSPropertyOperations;
@@ -102,9 +103,8 @@ describe('CSSPropertyOperations', function() {
display: null,
};
var div = <div style={styles} />;
var root = document.createElement('div');
ReactDOM.render(div, root);
expect(/style=".*"/.test(root.innerHTML)).toBe(false);
var html = ReactDOMServer.renderToString(div);
expect(/style=/.test(html)).toBe(false);
});
it('should warn when using hyphenated style names', function() {

View File

@@ -25,7 +25,7 @@ describe('Danger', function() {
Danger = require('Danger');
var ReactReconcileTransaction = require('ReactReconcileTransaction');
transaction = new ReactReconcileTransaction();
transaction = new ReactReconcileTransaction(/* forceHTML */ true);
});
it('should render markup', function() {

View File

@@ -69,7 +69,7 @@ function ReactUpdatesFlushTransaction() {
this.dirtyComponentsLength = null;
this.callbackQueue = CallbackQueue.getPooled();
this.reconcileTransaction =
ReactUpdates.ReactReconcileTransaction.getPooled();
ReactUpdates.ReactReconcileTransaction.getPooled(/* forceHTML */ false);
}
assign(

View File

@@ -165,6 +165,9 @@ describe('ReactMultiChild', function() {
}
beforeEach(function() {
var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
ReactDOMFeatureFlags.useCreateElement = false;
Object.defineProperty(Element.prototype, 'innerHTML', {
set: setInnerHTML = jasmine.createSpy().andCallFake(
innerHTMLDescriptor.set

View File

@@ -23,8 +23,6 @@ var DOM_OPERATION_TYPES = {
SET_MARKUP: 'set innerHTML',
TEXT_CONTENT: 'set textContent',
'updatePropertyByID': 'update attribute',
'deletePropertyByID': 'delete attribute',
'updateStylesByID': 'update styles',
'dangerouslyReplaceNodeWithMarkupByID': 'replace',
};

View File

@@ -399,7 +399,7 @@ ReactShallowRenderer.prototype.render = function(element, context) {
if (!context) {
context = emptyObject;
}
var transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(false);
this._render(element, transaction, context);
ReactUpdates.ReactReconcileTransaction.release(transaction);
};