Skip to content

Commit

Permalink
Adds legacyNoAttributes setting
Browse files Browse the repository at this point in the history
Applies a number of optimizations that speed up defining a legacy element. These optimizations are mostly beneficial when a large number of elements are defined that are not used for initial render.

* Fixes an issue with `dedupingMixin` that was causing info to be cached on the resulting class even when no mixin should be applied.
* In Polymer.Class, avoids using `LegacyElementMixin(HTMLElement)` in favor of a cached `LegacyElement`.
* Copies `DisableUpgradeMixin` into Polymer's legacy class generation. This avoids the need to mix this on top of all legacy elements.
* Adds `legacyNoAttributes` setting which avoids setting `observedAttributes` and instead (1) applies the values of all attributes at create time, (2) patches set/removeAttribute so that they call attributeChangedCallback. This is faster since it avoids the work Polymer needs to do to calculate `observedAttributes`.
  • Loading branch information
Steven Orvell committed Dec 19, 2019
1 parent 5d130fa commit 8ef2cc7
Show file tree
Hide file tree
Showing 4 changed files with 322 additions and 13 deletions.
155 changes: 148 additions & 7 deletions lib/legacy/class.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
*/

import { LegacyElementMixin } from './legacy-element-mixin.js';
import { legacyOptimizations } from '../utils/settings.js';
import { DisableUpgradeMixin } from '../mixins/disable-upgrade-mixin.js';
import { legacyOptimizations, legacyNoObservedAttributes } from '../utils/settings.js';
import { wrap } from '../utils/wrap.js';

const DISABLED_ATTR = 'disable-upgrade';
let observedAttributesGetter;

const lifecycleProps = {
attached: true,
Expand Down Expand Up @@ -208,6 +211,22 @@ function mergeProperties(target, source) {
*/
function GenerateClassFromInfo(info, Base, behaviors) {

// Work around for closure bug #126934458. Using `super` in a property
// getter does not work so instead we search the Base prototype for an
// implementation of observedAttributes so that we can override and call
// the `super` getter. Note, this is done one time ever because we assume
// that `Base` is always comes from `Polymer.LegacyElementMixn`.
if (!observedAttributesGetter) {
let ctor = Base;
while (ctor && !observedAttributesGetter) {
const desc = Object.getOwnPropertyDescriptor(ctor, 'observedAttributes');
if (desc) {
observedAttributesGetter = desc.get;
}
ctor = Object.getPrototypeOf(ctor.prototype).constructor;
}
}

// manages behavior and lifecycle processing (filled in after class definition)
let behaviorList;
const lifecycle = {};
Expand Down Expand Up @@ -280,6 +299,14 @@ function GenerateClassFromInfo(info, Base, behaviors) {
* @return {void}
*/
created() {
// Pull all attribute values 1x if `legacyNoObservedAttributes` is set.
if (legacyNoObservedAttributes && this.hasAttributes()) {
const a = this.attributes;
for (let i=0, l=a.length; i < l; i++) {
const attr = a[i];
this.__attributeReaction(attr.name, undefined, attr.value)
}
}
super.created();
const list = lifecycle.created;
if (list) {
Expand All @@ -289,6 +316,32 @@ function GenerateClassFromInfo(info, Base, behaviors) {
}
}

__attributeReaction(name, oldValue, value) {
if ((this.__dataAttributes && this.__dataAttributes[name]) || name === DISABLED_ATTR) {
this.attributeChangedCallback(name, oldValue, value);
}
}

setAttribute(name, value) {
if (legacyNoObservedAttributes) {
const oldValue = this.getAttribute(name);
super.setAttribute(name, value);
this.__attributeReaction(name, oldValue, value);
} else {
super.setAttribute(name, value);
}
}

removeAttribute(name) {
if (legacyNoObservedAttributes) {
const oldValue = this.getAttribute(name);
super.removeAttribute(name);
this.__attributeReaction(name, oldValue, null);
} else {
super.removeAttribute(name);
}
}

/**
* @return {void}
*/
Expand Down Expand Up @@ -423,6 +476,95 @@ function GenerateClassFromInfo(info, Base, behaviors) {
}
}
}

// NOTE: Below is an inlined version of DisableUpgradeMixin. It is inlined
// as a performance optimization to avoid the need to place the mixin on
// top of every legacy element.
constructor() {
super();
/** @type {boolean|undefined} */
this.__isUpgradeDisabled;
}

static get observedAttributes() {
return legacyNoObservedAttributes ? [] :
observedAttributesGetter.call(this).concat(DISABLED_ATTR);
}

// Prevent element from initializing properties when it's upgrade disabled.
/** @override */
_initializeProperties() {
if (this.hasAttribute(DISABLED_ATTR)) {
this.__isUpgradeDisabled = true;
} else {
super._initializeProperties();
}
}

// Prevent element from enabling properties when it's upgrade disabled.
// Normally overriding connectedCallback would be enough, but dom-* elements
/** @override */
_enableProperties() {
if (!this.__isUpgradeDisabled) {
super._enableProperties();
}
}

// If the element starts upgrade-disabled and a property is set for
// which an accessor exists, the default should not be applied.
// This additional check is needed because defaults are applied via
// `_initializeProperties` which is called after initial properties
// have been set when the element starts upgrade-disabled.
/** @override */
_canApplyPropertyDefault(property) {
return super._canApplyPropertyDefault(property) &&
!(this.__isUpgradeDisabled && this._isPropertyPending(property));
}

/**
* @override
* @param {string} name Attribute name.
* @param {?string} old The previous value for the attribute.
* @param {?string} value The new value for the attribute.
* @param {?string} namespace The XML namespace for the attribute.
* @return {void}
*/
attributeChangedCallback(name, old, value, namespace) {
if (name == DISABLED_ATTR) {
// When disable-upgrade is removed, intialize properties and
// provoke connectedCallback if the element is already connected.
if (this.__isUpgradeDisabled && value == null) {
super._initializeProperties();
this.__isUpgradeDisabled = false;
if (wrap(this).isConnected) {
super.connectedCallback();
}
}
} else {
super.attributeChangedCallback(
name, old, value, /** @type {null|string} */ (namespace));
}
}

// Prevent element from connecting when it's upgrade disabled.
// This prevents user code in `attached` from being called.
/** @override */
connectedCallback() {
if (!this.__isUpgradeDisabled) {
super.connectedCallback();
}
}

// Prevent element from disconnecting when it's upgrade disabled.
// This avoids allowing user code `detached` from being called without a
// paired call to `attached`.
/** @override */
disconnectedCallback() {
if (!this.__isUpgradeDisabled) {
super.disconnectedCallback();
}
}

}

// apply behaviors, note actual copying is done lazily at first instance creation
Expand Down Expand Up @@ -457,6 +599,8 @@ function GenerateClassFromInfo(info, Base, behaviors) {
return PolymerGenerated;
}

const LegacyElement = LegacyElementMixin(HTMLElement);

/**
* Generates a class that extends `LegacyElement` based on the
* provided info object. Metadata objects on the `info` object
Expand Down Expand Up @@ -531,13 +675,10 @@ export const Class = function(info, mixin) {
if (!info) {
console.warn('Polymer.Class requires `info` argument');
}
let klass = mixin ? mixin(LegacyElementMixin(HTMLElement)) :
LegacyElementMixin(HTMLElement);
let klass = mixin ? mixin(LegacyElement) :
LegacyElement;
klass = GenerateClassFromInfo(info, klass, info.behaviors);
// decorate klass with registration info
klass.is = klass.prototype.is = info.is;
if (legacyOptimizations) {
klass = DisableUpgradeMixin(klass);
}
return klass;
};
12 changes: 6 additions & 6 deletions lib/utils/mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ export const dedupingMixin = function(mixin) {
if (!extended) {
extended = /** @type {!Function} */(mixin)(base);
map.set(base, extended);
// copy inherited mixin set from the extended class, or the base class
// NOTE: we avoid use of Set here because some browser (IE11)
// cannot extend a base Set via the constructor.
let mixinSet = Object.create(/** @type {!MixinFunction} */(extended).__mixinSet || baseSet || null);
mixinSet[mixinDedupeId] = true;
/** @type {!MixinFunction} */(extended).__mixinSet = mixinSet;
}
// copy inherited mixin set from the extended class, or the base class
// NOTE: we avoid use of Set here because some browser (IE11)
// cannot extend a base Set via the constructor.
let mixinSet = Object.create(/** @type {!MixinFunction} */(extended).__mixinSet || baseSet || null);
mixinSet[mixinDedupeId] = true;
/** @type {!MixinFunction} */(extended).__mixinSet = mixinSet;
return extended;
}

Expand Down
18 changes: 18 additions & 0 deletions lib/utils/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -312,3 +312,21 @@ export let suppressTemplateNotifications =
export const setSuppressTemplateNotifications = function(suppress) {
suppressTemplateNotifications = suppress;
};

/**
* Setting to disable use of dynamic attributes. This is an optimization
* to avoid setting `observedAttributes`. Instead attributes are read
* once at create time.
*/
export let legacyNoObservedAttributes =
window.Polymer && window.Polymer.legacyNoObservedAttributes || false;

/**
* Sets `legacyNoObservedAttributes` globally, to disable `observedAttributes`.
*
* @param {boolean} disables `observedAttributes`
* @return {void}
*/
export const setLegacyNoObservedAttributes = function(noAttributes) {
legacyNoObservedAttributes = noAttributes;
};
150 changes: 150 additions & 0 deletions test/unit/legacy-noattributes.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
<!doctype html>
<!--
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<html>
<head>
<meta charset="utf-8">
<script src="../../node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>
<script src="wct-browser-config.js"></script>
<script src="../../node_modules/wct-browser-legacy/browser.js"></script>
<script type="module">
import {setLegacyNoObservedAttributes} from '../../lib/utils/settings.js';
setLegacyNoObservedAttributes(true);
</script>
</head>
<body>

<dom-module id="x-child">
<template>
<div id="content">[[foo]]</div>
</template>
<script type="module">
import {Polymer} from '../../polymer-legacy.js';
Polymer({
is: 'x-child',
properties: {
foo: {type: String, value: 'default'},
bar: {type: String, value: 'default'},
zot: {type: Boolean, value: false, reflectToAttribute: true}
},
ready() {
this.isEnabled = true;
}
});
</script>
</dom-module>

<dom-module id="x-attrs">
<template>
<x-child id="child1" foo="x-attrs.foo" bar$="[[bar]]" zot$="[[zot]]"></x-child>
<x-child id="child2" foo="x-attrs.foo" bar$="[[bar]]" zot$="[[zot]]" disable-upgrade></x-child>
<x-child id="child3" foo="x-attrs.foo" bar$="[[bar]]" zot$="[[zot]]" disable-upgrade$="[[disabled]]"></x-child>
<div id="ifContainer">
<dom-if if="[[shouldIf]]">
<template><x-child foo="x-attrs.foo" bar$="[[bar]]" zot$="[[zot]]"></x-child></template>
</dom-if>
</div>
</template>
<script type="module">
import {Polymer} from '../../polymer-legacy.js';
Polymer({
is: 'x-attrs',
properties: {
foo: String,
bar: {type: String, value: 'bar'},
zot: Boolean,
shouldIf: Boolean,
disabled: {type: Boolean, value: 'true'}
}
});
</script>
</dom-module>

<test-fixture id="declarative">
<template>
<x-attrs id="configured" foo="foo" bar="bar"></x-attrs>
</template>
</test-fixture>

<script type="module">
import {flush} from '../../lib/utils/flush.js';
import {wrap} from '../../lib/utils/wrap.js';

let el = window.configured;

suite('legacyNoObservedAttributes', () => {

let el;
setup(() => {
el = fixture('declarative');
});

test('static attributes', () => {
assert.equal(el.foo, 'foo');
assert.equal(el.$.child1.getAttribute('foo'), 'x-attrs.foo');
assert.equal(el.$.child1.foo, "x-attrs.foo");
assert.equal(el.$.child1.$.content.textContent, 'x-attrs.foo');
});

test('static attribute bindings', () => {
assert.equal(el.$.child1.getAttribute('bar'), 'bar');
assert.equal(el.$.child1.bar, 'bar');
assert.equal(el.$.child1.getAttribute('zot'), null);
assert.equal(el.$.child1.zot, false);
});

test('dynamic attribute bindings', () => {
el.bar = 'bar-modified';
assert.equal(el.$.child1.getAttribute('bar'), 'bar-modified');
assert.equal(el.$.child1.bar, 'bar-modified');
el.zot = true;
assert.equal(el.$.child1.getAttribute('zot'), '');
assert.equal(el.$.child1.zot, true);
});

test('attributes inside dom-if', () => {
el.shouldIf = true;
flush();
const child = wrap(el.$.ifContainer).querySelector('x-child');
assert.equal(child.getAttribute('foo'), 'x-attrs.foo');
assert.equal(child.foo, "x-attrs.foo");
assert.equal(child.$.content.textContent, 'x-attrs.foo');
//
assert.equal(child.getAttribute('bar'), 'bar');
assert.equal(child.bar, 'bar');
assert.equal(child.getAttribute('zot'), null);
assert.equal(child.zot, false);
//
el.bar = 'bar-modified';
assert.equal(child.getAttribute('bar'), 'bar-modified');
assert.equal(child.bar, 'bar-modified');
el.zot = true;
assert.equal(child.getAttribute('zot'), '');
assert.equal(child.zot, true);
});

test('disable-upgrade static', () => {
assert.notOk(el.$.child2.isEnabled);
el.$.child2.removeAttribute('disable-upgrade');
assert.isTrue(el.$.child2.isEnabled);
});

test('disable-upgrade binding', () => {
assert.notOk(el.$.child3.isEnabled);
el.disabled = false;
assert.isTrue(el.$.child3.isEnabled);
});


});

</script>
</body>
</html>

0 comments on commit 8ef2cc7

Please sign in to comment.