Skip to content

Commit

Permalink
Improve createProps perf (#4007)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pessimistress committed Dec 12, 2019
1 parent a26f4b6 commit 5b0dc71
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 119 deletions.
8 changes: 5 additions & 3 deletions modules/core/src/lifecycle/component-state.js
Expand Up @@ -21,6 +21,8 @@
import log from '../utils/log';
import assert from '../utils/assert';
import {isAsyncIterable} from '../utils/iterable-utils';
import {PROP_SYMBOLS} from './constants';
const {ASYNC_ORIGINAL, ASYNC_RESOLVED, ASYNC_DEFAULTS} = PROP_SYMBOLS;

const EMPTY_PROPS = Object.freeze({});

Expand Down Expand Up @@ -89,9 +91,9 @@ export default class ComponentState {
// Checks if urls have changed, starts loading, or removes override
setAsyncProps(props) {
// NOTE: prop param and default values are only support for testing
const resolvedValues = props._asyncPropResolvedValues || {};
const originalValues = props._asyncPropOriginalValues || props;
const defaultValues = props._asyncPropDefaultValues || {};
const resolvedValues = props[ASYNC_RESOLVED] || {};
const originalValues = props[ASYNC_ORIGINAL] || props;
const defaultValues = props[ASYNC_DEFAULTS] || {};

// TODO - use async props from the layer's prop types
for (const propName in resolvedValues) {
Expand Down
18 changes: 8 additions & 10 deletions modules/core/src/lifecycle/component.js
@@ -1,9 +1,7 @@
import {LIFECYCLE} from '../lifecycle/constants';
import {createProps} from '../lifecycle/create-props';
// import {diffProps} from '../lifecycle/props';
// import log from '../utils/log';
// import assert from '../utils/assert';

import {createProps} from './create-props';
import {PROP_SYMBOLS} from './constants';
const {ASYNC_ORIGINAL, ASYNC_RESOLVED, ASYNC_DEFAULTS} = PROP_SYMBOLS;
import ComponentState from './component-state';

const defaultProps = {};
Expand Down Expand Up @@ -38,11 +36,11 @@ export default class Component {
const asyncProps = {};

// See async props definition in create-props.js
for (const key in props._asyncPropDefaultValues) {
if (key in props._asyncPropResolvedValues) {
asyncProps[key] = props._asyncPropResolvedValues[key];
} else if (key in props._asyncPropOriginalValues) {
asyncProps[key] = props._asyncPropOriginalValues[key];
for (const key in props[ASYNC_DEFAULTS]) {
if (key in props[ASYNC_RESOLVED]) {
asyncProps[key] = props[ASYNC_RESOLVED][key];
} else if (key in props[ASYNC_ORIGINAL]) {
asyncProps[key] = props[ASYNC_ORIGINAL][key];
}
}

Expand Down
11 changes: 11 additions & 0 deletions modules/core/src/lifecycle/constants.js
Expand Up @@ -6,3 +6,14 @@ export const LIFECYCLE = {
AWAITING_FINALIZATION: 'No longer matched. Awaiting garbage collection',
FINALIZED: 'Finalized! Awaiting garbage collection'
};

/* Secret props keys */
// Symbols are non-enumerable by default, does not show in for...in or Object.keys
// but are copied with Object.assign ¯\_(ツ)_/¯
// Supported everywhere except IE11, can be polyfilled with core-js
export const PROP_SYMBOLS = {
COMPONENT: Symbol('component'),
ASYNC_DEFAULTS: Symbol('asyncPropDefaults'),
ASYNC_ORIGINAL: Symbol('asyncPropOriginal'),
ASYNC_RESOLVED: Symbol('asyncPropResolved')
};
178 changes: 74 additions & 104 deletions modules/core/src/lifecycle/create-props.js
@@ -1,139 +1,100 @@
import log from '../utils/log';
import {isAsyncIterable} from '../utils/iterable-utils';
import {parsePropTypes} from './prop-types';
import {PROP_SYMBOLS} from './constants';

const {COMPONENT, ASYNC_ORIGINAL, ASYNC_RESOLVED, ASYNC_DEFAULTS} = PROP_SYMBOLS;

// Create a property object
export function createProps() {
const component = this; // eslint-disable-line

// Get default prop object (a prototype chain for now)
const propTypeDefs = getPropsPrototypeAndTypes(component.constructor);
const propsPrototype = propTypeDefs.defaultProps;
const propsPrototype = getPropsPrototype(component.constructor);

// Create a new prop object with default props object in prototype chain
const propsInstance = Object.create(propsPrototype, {
// Props need a back pointer to the owning component
_component: {
enumerable: false,
value: component
},
// The supplied (original) values for those async props that are set to url strings or Promises.
// In this case, the actual (i.e. resolved) values are looked up from component.internalState
_asyncPropOriginalValues: {
enumerable: false,
value: {}
},
// Note: the actual (resolved) values for props that are NOT set to urls or Promises.
// in this case the values are served directly from this map
_asyncPropResolvedValues: {
enumerable: false,
value: {}
}
});
const propsInstance = Object.create(propsPrototype);

// Props need a back pointer to the owning component
propsInstance[COMPONENT] = component;
// The supplied (original) values for those async props that are set to url strings or Promises.
// In this case, the actual (i.e. resolved) values are looked up from component.internalState
propsInstance[ASYNC_ORIGINAL] = {};
// Note: the actual (resolved) values for props that are NOT set to urls or Promises.
// in this case the values are served directly from this map
propsInstance[ASYNC_RESOLVED] = {};

// "Copy" all sync props
for (let i = 0; i < arguments.length; ++i) {
Object.assign(propsInstance, arguments[i]);
const props = arguments[i];
// Do not use Object.assign here to avoid Symbols in props overwriting our private fields
// This might happen if one of the arguments is another props instance
for (const key in props) {
propsInstance[key] = props[key];
}
}

const {layerName} = component.constructor;
const {deprecatedProps} = propTypeDefs;
checkDeprecatedProps(layerName, propsInstance, deprecatedProps);
checkDeprecatedProps(layerName, propsInstance.updateTriggers, deprecatedProps);
checkDeprecatedProps(layerName, propsInstance.transitions, deprecatedProps);

// Props must be immutable
Object.freeze(propsInstance);

return propsInstance;
}

/* eslint-disable max-depth */
function checkDeprecatedProps(layerName, propsInstance, deprecatedProps) {
if (!propsInstance) {
return;
}

for (const name in deprecatedProps) {
if (hasOwnProperty(propsInstance, name)) {
const nameStr = `${layerName || 'Layer'}: ${name}`;

for (const newPropName of deprecatedProps[name]) {
if (!hasOwnProperty(propsInstance, newPropName)) {
propsInstance[newPropName] = propsInstance[name];
}
}

log.deprecated(nameStr, deprecatedProps[name].join('/'))();
}
}
}
/* eslint-enable max-depth */

// Return precalculated defaultProps and propType objects if available
// build them if needed
function getPropsPrototypeAndTypes(componentClass) {
const props = getOwnProperty(componentClass, '_mergedDefaultProps');
if (props) {
return {
defaultProps: props,
propTypes: getOwnProperty(componentClass, '_propTypes'),
deprecatedProps: getOwnProperty(componentClass, '_deprecatedProps')
};
function getPropsPrototype(componentClass) {
const defaultProps = getOwnProperty(componentClass, '_mergedDefaultProps');
if (!defaultProps) {
createPropsPrototypeAndTypes(componentClass);
return componentClass._mergedDefaultProps;
}

return createPropsPrototypeAndTypes(componentClass);
return defaultProps;
}

// Build defaultProps and propType objects by walking component prototype chain
function createPropsPrototypeAndTypes(componentClass) {
const parent = componentClass.prototype;
if (!parent) {
return {
defaultProps: {}
};
return;
}

const parentClass = Object.getPrototypeOf(componentClass);
const parentPropDefs = (parent && getPropsPrototypeAndTypes(parentClass)) || null;
const parentDefaultProps = getPropsPrototype(parentClass);

// Parse propTypes from Component.defaultProps
const componentDefaultProps = getOwnProperty(componentClass, 'defaultProps') || {};
const componentPropDefs = parsePropTypes(componentDefaultProps);

// Create a merged type object
const propTypes = Object.assign(
{},
parentPropDefs && parentPropDefs.propTypes,
componentPropDefs.propTypes
);

// Create any necessary property descriptors and create the default prop object
// Assign merged default props
const defaultProps = createPropsPrototype(
componentPropDefs.defaultProps,
parentPropDefs && parentPropDefs.defaultProps,
propTypes,
parentDefaultProps,
componentClass
);

// Create a merged type object
const propTypes = Object.assign({}, parentClass._propTypes, componentPropDefs.propTypes);
// Add getters/setters for async props
addAsyncPropsToPropPrototype(defaultProps, propTypes);

// Create a map for prop whose default value is a callback
const deprecatedProps = Object.assign(
{},
parentPropDefs && parentPropDefs.deprecatedProps,
parentClass._deprecatedProps,
componentPropDefs.deprecatedProps
);
// Add setters for deprecated props
addDeprecatedPropsToPropPrototype(defaultProps, deprecatedProps);

// Store the precalculated props
componentClass._mergedDefaultProps = defaultProps;
componentClass._propTypes = propTypes;
componentClass._deprecatedProps = deprecatedProps;

return {propTypes, defaultProps, deprecatedProps};
}

// Builds a pre-merged default props object that component props can inherit from
function createPropsPrototype(props, parentProps, propTypes, componentClass) {
function createPropsPrototype(props, parentProps, componentClass) {
const defaultProps = Object.create(null);

Object.assign(defaultProps, parentProps, props);
Expand All @@ -142,38 +103,43 @@ function createPropsPrototype(props, parentProps, propTypes, componentClass) {
const id = getComponentName(componentClass);
delete props.id;

// Add getters/setters for async prop properties
Object.defineProperties(defaultProps, {
// `id` is treated specially because layer might need to override it
id: {
configurable: false,
writable: true,
value: id
}
});

// Add getters/setters for async prop properties
addAsyncPropsToPropPrototype(defaultProps, propTypes);

return defaultProps;
}

function addDeprecatedPropsToPropPrototype(defaultProps, deprecatedProps) {
for (const propName in deprecatedProps) {
/* eslint-disable accessor-pairs */
Object.defineProperty(defaultProps, propName, {
enumerable: false,
set(newValue) {
const nameStr = `${this.id}: ${propName}`;

for (const newPropName of deprecatedProps[propName]) {
if (!hasOwnProperty(this, newPropName)) {
this[newPropName] = newValue;
}
}

log.deprecated(nameStr, deprecatedProps[propName].join('/'))();
}
});
/* eslint-enable accessor-pairs */
}
}

// Create descriptors for overridable props
function addAsyncPropsToPropPrototype(defaultProps, propTypes) {
const defaultValues = {};

const descriptors = {
// Default "resolved" values for async props, returned if value not yet resolved/set.
_asyncPropDefaultValues: {
enumerable: false,
value: defaultValues
},
// Shadowed object, just to make sure "early indexing" into the instance does not fail
_asyncPropOriginalValues: {
enumerable: false,
value: {}
}
};
const descriptors = {};

// Move async props into shadow values
for (const propName in propTypes) {
Expand All @@ -187,13 +153,17 @@ function addAsyncPropsToPropPrototype(defaultProps, propTypes) {
}
}

// Default "resolved" values for async props, returned if value not yet resolved/set.
defaultProps[ASYNC_DEFAULTS] = defaultValues;
// Shadowed object, just to make sure "early indexing" into the instance does not fail
defaultProps[ASYNC_ORIGINAL] = {};

Object.defineProperties(defaultProps, descriptors);
}

// Helper: Configures getter and setter for one async prop
function getDescriptorForAsyncProp(name) {
return {
configurable: false,
enumerable: true,
// Save the provided value for async props in a special map
set(newValue) {
Expand All @@ -202,29 +172,29 @@ function getDescriptorForAsyncProp(name) {
newValue instanceof Promise ||
isAsyncIterable(newValue)
) {
this._asyncPropOriginalValues[name] = newValue;
this[ASYNC_ORIGINAL][name] = newValue;
} else {
this._asyncPropResolvedValues[name] = newValue;
this[ASYNC_RESOLVED][name] = newValue;
}
},
// Only the component's state knows the true value of async prop
get() {
if (this._asyncPropResolvedValues) {
if (this[ASYNC_RESOLVED]) {
// Prop value isn't async, so just return it
if (name in this._asyncPropResolvedValues) {
const value = this._asyncPropResolvedValues[name];
if (name in this[ASYNC_RESOLVED]) {
const value = this[ASYNC_RESOLVED][name];

// Special handling - components expect null `data` prop expects to be replaced with `[]`
if (name === 'data') {
return value || this._asyncPropDefaultValues[name];
return value || this[ASYNC_DEFAULTS][name];
}

return value;
}

if (name in this._asyncPropOriginalValues) {
if (name in this[ASYNC_ORIGINAL]) {
// It's an async prop value: look into component state
const state = this._component && this._component.internalState;
const state = this[COMPONENT] && this[COMPONENT].internalState;
if (state && state.hasAsyncProp(name)) {
return state.getAsyncProp(name);
}
Expand All @@ -233,7 +203,7 @@ function getDescriptorForAsyncProp(name) {

// the prop is not supplied, or
// component not yet initialized/matched, return the component's default value for the prop
return this._asyncPropDefaultValues[name];
return this[ASYNC_DEFAULTS][name];
}
};
}
Expand Down
5 changes: 4 additions & 1 deletion modules/core/src/lifecycle/props.js
@@ -1,4 +1,7 @@
import assert from '../utils/assert';
import {PROP_SYMBOLS} from './constants';

const {COMPONENT} = PROP_SYMBOLS;

export function validateProps(props) {
const propTypes = getPropTypes(props);
Expand Down Expand Up @@ -246,7 +249,7 @@ function diffUpdateTrigger(props, oldProps, triggerName) {
}

function getPropTypes(props) {
const layer = props._component;
const layer = props[COMPONENT];
const LayerType = layer && layer.constructor;
return LayerType ? LayerType._propTypes : {};
}
2 changes: 1 addition & 1 deletion modules/layers/src/icon-layer/icon-layer.js
Expand Up @@ -127,7 +127,7 @@ export default class IconLayer extends Layer {
const {iconAtlas, iconMapping, data, getIcon} = props;

let iconMappingChanged = false;
const prePacked = iconAtlas || this.props._asyncPropOriginalValues.iconAtlas;
const prePacked = iconAtlas || this.internalState.isAsyncPropLoading('iconAtlas');

// prepacked iconAtlas from user
if (prePacked) {
Expand Down

0 comments on commit 5b0dc71

Please sign in to comment.