Files
react/src/core/ReactDescriptor.js
T
Sebastian Markbage d0719a5ea4 Preparing to move defaultProps resolution and type validation to the descriptor
This copies the propType and contextType validation to a wrapper around the
descriptor factory. By doing the validation early, we make it easier to track
down bugs. It also prepares for static type checking which should be done at the
usage site.

This validation is not yet active and is just logged using monitorCodeUse. This
will allow us to clean up callsites which would fail this new type of
validation.

I chose to copy the validation of abstracting it out since this is just an
intermediate step to avoid spamming consoles. This makes more a much cleaner
diff review/history. The original validation in the instance will be deleted as
soon as we can turn on the warnings.

Additionally, getDefaultProps are moved to become a static function which is
only executed once. It should be moved to statics but we don't have a
convenient way to merge mixins in statics right now. Deferring to ES6 classes.

This is still a breaking change since you can return an object or array from
getDefaultProps, which later gets mutated and now the shared instance is
mutated. Mutating an object that is passed into you from props is highly
discouraged and likely to lead to subtle bugs anyway. So I'm not too worried.

The defaultProps will later be resolved in the descriptor factory. This will
enable a perf optimizations where we don't create an unnecessary object
allocation when you use default props. It also means that ReactChildren.map
has access to resolved properties which gives them consistent behavior whether
or not the default prop is specified.
2014-06-26 15:40:12 -07:00

244 lines
6.9 KiB
JavaScript

/**
* Copyright 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 ReactDescriptor
*/
"use strict";
var ReactContext = require('ReactContext');
var ReactCurrentOwner = require('ReactCurrentOwner');
var merge = require('merge');
var warning = require('warning');
/**
* Warn for mutations.
*
* @internal
* @param {object} object
* @param {string} key
*/
function defineWarningProperty(object, key) {
Object.defineProperty(object, key, {
configurable: false,
enumerable: true,
get: function() {
if (!this._store) {
return null;
}
return this._store[key];
},
set: function(value) {
warning(
false,
'Don\'t set the ' + key + ' property of the component. ' +
'Mutate the existing props object instead.'
);
this._store[key] = value;
}
});
}
/**
* This is updated to true if the membrane is successfully created.
*/
var useMutationMembrane = false;
/**
* Warn for mutations.
*
* @internal
* @param {object} descriptor
*/
function defineMutationMembrane(prototype) {
try {
var pseudoFrozenProperties = {
props: true
};
for (var key in pseudoFrozenProperties) {
defineWarningProperty(prototype, key);
}
useMutationMembrane = true;
} catch (x) {
// IE will fail on defineProperty
}
}
/**
* Transfer static properties from the source to the target. Functions are
* rebound to have this reflect the original source.
*/
function proxyStaticMethods(target, source) {
if (typeof source !== 'function') {
return;
}
for (var key in source) {
if (source.hasOwnProperty(key)) {
var value = source[key];
if (typeof value === 'function') {
target[key] = value.bind(source);
} else {
target[key] = value;
}
}
}
}
/**
* Base constructor for all React descriptors. This is only used to make this
* work with a dynamic instanceof check. Nothing should live on this prototype.
*
* @param {*} type
* @internal
*/
var ReactDescriptor = function() {};
if (__DEV__) {
defineMutationMembrane(ReactDescriptor.prototype);
}
ReactDescriptor.createFactory = function(type) {
var descriptorPrototype = Object.create(ReactDescriptor.prototype);
var factory = function(props, children) {
// For consistency we currently allocate a new object for every descriptor.
// This protects the descriptor from being mutated by the original props
// object being mutated. It also protects the original props object from
// being mutated by children arguments and default props. This behavior
// comes with a performance cost and could be deprecated in the future.
// It could also be optimized with a smarter JSX transform.
if (props == null) {
props = {};
} else if (typeof props === 'object') {
props = merge(props);
}
// Children can be more than one argument, and those are transferred onto
// the newly allocated props object.
var childrenLength = arguments.length - 1;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
var childArray = Array(childrenLength);
for (var i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 1];
}
props.children = childArray;
}
// Initialize the descriptor object
var descriptor = Object.create(descriptorPrototype);
// Record the component responsible for creating this descriptor.
descriptor._owner = ReactCurrentOwner.current;
// TODO: Deprecate withContext, and then the context becomes accessible
// through the owner.
descriptor._context = ReactContext.current;
if (__DEV__) {
// The validation flag and props are currently mutative. We put them on
// an external backing store so that we can freeze the whole object.
// This can be replaced with a WeakMap once they are implemented in
// commonly used development environments.
descriptor._store = { validated: false, props: props };
// We're not allowed to set props directly on the object so we early
// return and rely on the prototype membrane to forward to the backing
// store.
if (useMutationMembrane) {
Object.freeze(descriptor);
return descriptor;
}
}
descriptor.props = props;
return descriptor;
};
// Currently we expose the prototype of the descriptor so that
// <Foo /> instanceof Foo works. This is controversial pattern.
factory.prototype = descriptorPrototype;
// Expose the type on the factory and the prototype so that it can be
// easily accessed on descriptors. E.g. <Foo />.type === Foo.type and for
// static methods like <Foo />.type.staticMethod();
// This should not be named constructor since this may not be the function
// that created the descriptor, and it may not even be a constructor.
factory.type = type;
descriptorPrototype.type = type;
proxyStaticMethods(factory, type);
// Expose a unique constructor on the prototype is that this works with type
// systems that compare constructor properties: <Foo />.constructor === Foo
// This may be controversial since it requires a known factory function.
descriptorPrototype.constructor = factory;
return factory;
};
ReactDescriptor.cloneAndReplaceProps = function(oldDescriptor, newProps) {
var newDescriptor = Object.create(oldDescriptor.constructor.prototype);
// It's important that this property order matches the hidden class of the
// original descriptor to maintain perf.
newDescriptor._owner = oldDescriptor._owner;
newDescriptor._context = oldDescriptor._context;
if (__DEV__) {
newDescriptor._store = {
validated: oldDescriptor._store.validated,
props: newProps
};
if (useMutationMembrane) {
Object.freeze(newDescriptor);
return newDescriptor;
}
}
newDescriptor.props = newProps;
return newDescriptor;
};
/**
* Checks if a value is a valid descriptor constructor.
*
* @param {*}
* @return {boolean}
* @public
*/
ReactDescriptor.isValidFactory = function(factory) {
return typeof factory === 'function' &&
factory.prototype instanceof ReactDescriptor;
};
/**
* @param {?object} object
* @return {boolean} True if `object` is a valid component.
* @final
*/
ReactDescriptor.isValidDescriptor = function(object) {
return object instanceof ReactDescriptor;
};
module.exports = ReactDescriptor;