refactor(render): don’t use a global cache for property setters

Related to #2359
This commit is contained in:
Tobias Bosch 2015-06-05 11:10:07 -07:00
parent 46eeee6b5e
commit 87b3b718e3
4 changed files with 141 additions and 134 deletions

View File

@ -16,20 +16,25 @@ import {TemplateLoader} from 'angular2/src/render/dom/compiler/template_loader';
import {CompileStepFactory, DefaultStepFactory} from './compile_step_factory';
import {Parser} from 'angular2/change_detection';
import {ShadowDomStrategy} from '../shadow_dom/shadow_dom_strategy';
import {
PropertySetterFactory
} from '../view/property_setter_factory'
/**
* The compiler loads and translates the html templates of components into
* nested ProtoViews. To decompose its functionality it uses
* the CompilePipeline and the CompileSteps.
*/
export class DomCompiler extends RenderCompiler {
/**
* The compiler loads and translates the html templates of components into
* nested ProtoViews. To decompose its functionality it uses
* the CompilePipeline and the CompileSteps.
*/
export class DomCompiler extends RenderCompiler {
_templateLoader: TemplateLoader;
_stepFactory: CompileStepFactory;
_propertySetterFactory: PropertySetterFactory;
constructor(stepFactory: CompileStepFactory, templateLoader: TemplateLoader) {
super();
this._templateLoader = templateLoader;
this._stepFactory = stepFactory;
this._propertySetterFactory = new PropertySetterFactory();
}
compile(template: ViewDefinition): Promise<ProtoViewDto> {
@ -58,7 +63,7 @@ export class DomCompiler extends RenderCompiler {
var pipeline = new CompilePipeline(this._stepFactory.createSteps(viewDef, subTaskPromises));
var compileElements = pipeline.process(tplElement, protoViewType, viewDef.componentId);
var protoView = compileElements[0].inheritedProtoView.build();
var protoView = compileElements[0].inheritedProtoView.build(this._propertySetterFactory);
if (subTaskPromises.length > 0) {
return PromiseWrapper.all(subTaskPromises).then((_) => protoView);

View File

@ -13,119 +13,123 @@ import {camelCaseToDashCase, dashCaseToCamelCase} from '../util';
import {reflector} from 'angular2/src/reflection/reflection';
const STYLE_SEPARATOR = '.';
var propertySettersCache = StringMapWrapper.create();
var innerHTMLSetterCache;
const ATTRIBUTE_PREFIX = 'attr.';
var attributeSettersCache = StringMapWrapper.create();
const CLASS_PREFIX = 'class.';
var classSettersCache = StringMapWrapper.create();
const STYLE_PREFIX = 'style.';
var styleSettersCache = StringMapWrapper.create();
export function setterFactory(property: string): Function {
var setterFn, styleParts, styleSuffix;
if (StringWrapper.startsWith(property, ATTRIBUTE_PREFIX)) {
setterFn = attributeSetterFactory(StringWrapper.substring(property, ATTRIBUTE_PREFIX.length));
} else if (StringWrapper.startsWith(property, CLASS_PREFIX)) {
setterFn = classSetterFactory(StringWrapper.substring(property, CLASS_PREFIX.length));
} else if (StringWrapper.startsWith(property, STYLE_PREFIX)) {
styleParts = property.split(STYLE_SEPARATOR);
styleSuffix = styleParts.length > 2 ? ListWrapper.get(styleParts, 2) : '';
setterFn = styleSetterFactory(ListWrapper.get(styleParts, 1), styleSuffix);
} else if (StringWrapper.equals(property, 'innerHtml')) {
if (isBlank(innerHTMLSetterCache)) {
innerHTMLSetterCache = (el, value) => DOM.setInnerHTML(el, value);
export class PropertySetterFactory {
private _propertySettersCache: StringMap<string, Function> = StringMapWrapper.create();
private _innerHTMLSetterCache: Function;
private _attributeSettersCache: StringMap<string, Function> = StringMapWrapper.create();
private _classSettersCache: StringMap<string, Function> = StringMapWrapper.create();
private _styleSettersCache: StringMap<string, Function> = StringMapWrapper.create();
createSetter(property: string): Function {
var setterFn, styleParts, styleSuffix;
if (StringWrapper.startsWith(property, ATTRIBUTE_PREFIX)) {
setterFn =
this._attributeSetterFactory(StringWrapper.substring(property, ATTRIBUTE_PREFIX.length));
} else if (StringWrapper.startsWith(property, CLASS_PREFIX)) {
setterFn = this._classSetterFactory(StringWrapper.substring(property, CLASS_PREFIX.length));
} else if (StringWrapper.startsWith(property, STYLE_PREFIX)) {
styleParts = property.split(STYLE_SEPARATOR);
styleSuffix = styleParts.length > 2 ? ListWrapper.get(styleParts, 2) : '';
setterFn = this._styleSetterFactory(ListWrapper.get(styleParts, 1), styleSuffix);
} else if (StringWrapper.equals(property, 'innerHtml')) {
if (isBlank(this._innerHTMLSetterCache)) {
this._innerHTMLSetterCache = (el, value) => DOM.setInnerHTML(el, value);
}
setterFn = this._innerHTMLSetterCache;
} else {
property = this._resolvePropertyName(property);
setterFn = StringMapWrapper.get(this._propertySettersCache, property);
if (isBlank(setterFn)) {
var propertySetterFn = reflector.setter(property);
setterFn = (receiver, value) => {
if (DOM.hasProperty(receiver, property)) {
return propertySetterFn(receiver, value);
}
};
StringMapWrapper.set(this._propertySettersCache, property, setterFn);
}
}
setterFn = innerHTMLSetterCache;
} else {
property = resolvePropertyName(property);
setterFn = StringMapWrapper.get(propertySettersCache, property);
return setterFn;
}
private _isValidAttributeValue(attrName: string, value: any): boolean {
if (attrName == "role") {
return isString(value);
} else {
return isPresent(value);
}
}
private _attributeSetterFactory(attrName: string): Function {
var setterFn = StringMapWrapper.get(this._attributeSettersCache, attrName);
var dashCasedAttributeName;
if (isBlank(setterFn)) {
var propertySetterFn = reflector.setter(property);
setterFn = function(receiver, value) {
if (DOM.hasProperty(receiver, property)) {
return propertySetterFn(receiver, value);
dashCasedAttributeName = camelCaseToDashCase(attrName);
setterFn = (element, value) => {
if (this._isValidAttributeValue(dashCasedAttributeName, value)) {
DOM.setAttribute(element, dashCasedAttributeName, stringify(value));
} else {
if (isPresent(value)) {
throw new BaseException("Invalid " + dashCasedAttributeName +
" attribute, only string values are allowed, got '" +
stringify(value) + "'");
}
DOM.removeAttribute(element, dashCasedAttributeName);
}
};
StringMapWrapper.set(propertySettersCache, property, setterFn);
StringMapWrapper.set(this._attributeSettersCache, attrName, setterFn);
}
return setterFn;
}
return setterFn;
}
function _isValidAttributeValue(attrName: string, value: any): boolean {
if (attrName == "role") {
return isString(value);
} else {
return isPresent(value);
}
}
function attributeSetterFactory(attrName: string): Function {
var setterFn = StringMapWrapper.get(attributeSettersCache, attrName);
var dashCasedAttributeName;
if (isBlank(setterFn)) {
dashCasedAttributeName = camelCaseToDashCase(attrName);
setterFn = function(element, value) {
if (_isValidAttributeValue(dashCasedAttributeName, value)) {
DOM.setAttribute(element, dashCasedAttributeName, stringify(value));
} else {
if (isPresent(value)) {
throw new BaseException("Invalid " + dashCasedAttributeName +
" attribute, only string values are allowed, got '" +
stringify(value) + "'");
private _classSetterFactory(className: string): Function {
var setterFn = StringMapWrapper.get(this._classSettersCache, className);
var dashCasedClassName;
if (isBlank(setterFn)) {
dashCasedClassName = camelCaseToDashCase(className);
setterFn = (element, value) => {
if (value) {
DOM.addClass(element, dashCasedClassName);
} else {
DOM.removeClass(element, dashCasedClassName);
}
DOM.removeAttribute(element, dashCasedAttributeName);
}
};
StringMapWrapper.set(attributeSettersCache, attrName, setterFn);
};
StringMapWrapper.set(this._classSettersCache, className, setterFn);
}
return setterFn;
}
return setterFn;
}
private _styleSetterFactory(styleName: string, styleSuffix: string): Function {
var cacheKey = styleName + styleSuffix;
var setterFn = StringMapWrapper.get(this._styleSettersCache, cacheKey);
var dashCasedStyleName;
function classSetterFactory(className: string): Function {
var setterFn = StringMapWrapper.get(classSettersCache, className);
var dashCasedClassName;
if (isBlank(setterFn)) {
dashCasedClassName = camelCaseToDashCase(className);
setterFn = function(element, value) {
if (value) {
DOM.addClass(element, dashCasedClassName);
} else {
DOM.removeClass(element, dashCasedClassName);
}
};
StringMapWrapper.set(classSettersCache, className, setterFn);
if (isBlank(setterFn)) {
dashCasedStyleName = camelCaseToDashCase(styleName);
setterFn = (element, value) => {
var valAsStr;
if (isPresent(value)) {
valAsStr = stringify(value);
DOM.setStyle(element, dashCasedStyleName, valAsStr + styleSuffix);
} else {
DOM.removeStyle(element, dashCasedStyleName);
}
};
StringMapWrapper.set(this._styleSettersCache, cacheKey, setterFn);
}
return setterFn;
}
return setterFn;
}
function styleSetterFactory(styleName: string, styleSuffix: string): Function {
var cacheKey = styleName + styleSuffix;
var setterFn = StringMapWrapper.get(styleSettersCache, cacheKey);
var dashCasedStyleName;
if (isBlank(setterFn)) {
dashCasedStyleName = camelCaseToDashCase(styleName);
setterFn = function(element, value) {
var valAsStr;
if (isPresent(value)) {
valAsStr = stringify(value);
DOM.setStyle(element, dashCasedStyleName, valAsStr + styleSuffix);
} else {
DOM.removeStyle(element, dashCasedStyleName);
}
};
StringMapWrapper.set(styleSettersCache, cacheKey, setterFn);
private _resolvePropertyName(attrName: string): string {
var mappedPropName = StringMapWrapper.get(DOM.attrToPropMap, attrName);
return isPresent(mappedPropName) ? mappedPropName : attrName;
}
return setterFn;
}
function resolvePropertyName(attrName: string): string {
var mappedPropName = StringMapWrapper.get(DOM.attrToPropMap, attrName);
return isPresent(mappedPropName) ? mappedPropName : attrName;
}

View File

@ -13,24 +13,17 @@ import {
import {DomProtoView, DomProtoViewRef, resolveInternalDomProtoView} from './proto_view';
import {ElementBinder, Event, HostAction} from './element_binder';
import {setterFactory} from './property_setter_factory';
import {PropertySetterFactory} from './property_setter_factory';
import * as api from '../../api';
import {NG_BINDING_CLASS, EVENT_TARGET_SEPARATOR} from '../util';
export class ProtoViewBuilder {
rootElement;
variableBindings: Map<string, string>;
elements: List<ElementBinderBuilder>;
type: number;
variableBindings: Map<string, string> = MapWrapper.create();
elements: List<ElementBinderBuilder> = [];
constructor(rootElement, type: number) {
this.rootElement = rootElement;
this.elements = [];
this.variableBindings = MapWrapper.create();
this.type = type;
}
constructor(public rootElement, public type: number) {}
bindElement(element, description = null): ElementBinderBuilder {
var builder = new ElementBinderBuilder(this.elements.length, element, description);
@ -50,7 +43,7 @@ export class ProtoViewBuilder {
MapWrapper.set(this.variableBindings, value, name);
}
build(): api.ProtoViewDto {
build(setterFactory: PropertySetterFactory): api.ProtoViewDto {
var renderElementBinders = [];
var apiElementBinders = [];
@ -63,7 +56,8 @@ export class ProtoViewBuilder {
ebb.eventBuilder.merge(dbb.eventBuilder);
MapWrapper.forEach(dbb.hostPropertyBindings, (_, hostPropertyName) => {
MapWrapper.set(propertySetters, hostPropertyName, setterFactory(hostPropertyName));
MapWrapper.set(propertySetters, hostPropertyName,
setterFactory.createSetter(hostPropertyName));
});
ListWrapper.forEach(dbb.hostActions, (hostAction) => {
@ -79,10 +73,11 @@ export class ProtoViewBuilder {
});
MapWrapper.forEach(ebb.propertyBindings, (_, propertyName) => {
MapWrapper.set(propertySetters, propertyName, setterFactory(propertyName));
MapWrapper.set(propertySetters, propertyName, setterFactory.createSetter(propertyName));
});
var nestedProtoView = isPresent(ebb.nestedProtoView) ? ebb.nestedProtoView.build() : null;
var nestedProtoView =
isPresent(ebb.nestedProtoView) ? ebb.nestedProtoView.build(setterFactory) : null;
var nestedRenderProtoView =
isPresent(nestedProtoView) ? resolveInternalDomProtoView(nestedProtoView.render) : null;
if (isPresent(nestedRenderProtoView)) {

View File

@ -9,25 +9,28 @@ import {
beforeEach,
el
} from 'angular2/test_lib';
import {setterFactory} from 'angular2/src/render/dom/view/property_setter_factory';
import {PropertySetterFactory} from 'angular2/src/render/dom/view/property_setter_factory';
import {DOM} from 'angular2/src/dom/dom_adapter';
export function main() {
var div;
beforeEach(() => { div = el('<div></div>'); });
var div, setterFactory;
beforeEach(() => {
div = el('<div></div>');
setterFactory = new PropertySetterFactory();
});
describe('property setter factory', () => {
it('should return a setter for a property', () => {
var setterFn = setterFactory('title');
var setterFn = setterFactory.createSetter('title');
setterFn(div, 'Hello');
expect(div.title).toEqual('Hello');
var otherSetterFn = setterFactory('title');
var otherSetterFn = setterFactory.createSetter('title');
expect(setterFn).toBe(otherSetterFn);
});
it('should return a setter for an attribute', () => {
var setterFn = setterFactory('attr.role');
var setterFn = setterFactory.createSetter('attr.role');
setterFn(div, 'button');
expect(DOM.getAttribute(div, 'role')).toEqual('button');
setterFn(div, null);
@ -35,49 +38,49 @@ export function main() {
expect(() => { setterFn(div, 4); })
.toThrowError("Invalid role attribute, only string values are allowed, got '4'");
var otherSetterFn = setterFactory('attr.role');
var otherSetterFn = setterFactory.createSetter('attr.role');
expect(setterFn).toBe(otherSetterFn);
});
it('should return a setter for a class', () => {
var setterFn = setterFactory('class.active');
var setterFn = setterFactory.createSetter('class.active');
setterFn(div, true);
expect(DOM.hasClass(div, 'active')).toEqual(true);
setterFn(div, false);
expect(DOM.hasClass(div, 'active')).toEqual(false);
var otherSetterFn = setterFactory('class.active');
var otherSetterFn = setterFactory.createSetter('class.active');
expect(setterFn).toBe(otherSetterFn);
});
it('should return a setter for a style', () => {
var setterFn = setterFactory('style.width');
var setterFn = setterFactory.createSetter('style.width');
setterFn(div, '40px');
expect(DOM.getStyle(div, 'width')).toEqual('40px');
setterFn(div, null);
expect(DOM.getStyle(div, 'width')).toEqual('');
var otherSetterFn = setterFactory('style.width');
var otherSetterFn = setterFactory.createSetter('style.width');
expect(setterFn).toBe(otherSetterFn);
});
it('should return a setter for a style with a unit', () => {
var setterFn = setterFactory('style.height.px');
var setterFn = setterFactory.createSetter('style.height.px');
setterFn(div, 40);
expect(DOM.getStyle(div, 'height')).toEqual('40px');
setterFn(div, null);
expect(DOM.getStyle(div, 'height')).toEqual('');
var otherSetterFn = setterFactory('style.height.px');
var otherSetterFn = setterFactory.createSetter('style.height.px');
expect(setterFn).toBe(otherSetterFn);
});
it('should return a setter for innerHtml', () => {
var setterFn = setterFactory('innerHtml');
var setterFn = setterFactory.createSetter('innerHtml');
setterFn(div, '<span></span>');
expect(DOM.getInnerHTML(div)).toEqual('<span></span>');
var otherSetterFn = setterFactory('innerHtml');
var otherSetterFn = setterFactory.createSetter('innerHtml');
expect(setterFn).toBe(otherSetterFn);
});