diff --git a/packages/@ember/-internals/metal/lib/computed.ts b/packages/@ember/-internals/metal/lib/computed.ts index b2f872e6419..6df592695df 100644 --- a/packages/@ember/-internals/metal/lib/computed.ts +++ b/packages/@ember/-internals/metal/lib/computed.ts @@ -253,8 +253,8 @@ function noop(): void {} @public */ export class ComputedProperty extends ComputedDescriptor { - protected _volatile = false; - protected _readOnly = false; + _volatile = false; + _readOnly = false; protected _hasConfig = false; _getter?: ComputedPropertyGetter = undefined; @@ -352,182 +352,6 @@ export class ComputedProperty extends ComputedDescriptor { } } - /** - Call on a computed property to set it into non-cached mode. When in this - mode the computed property will not automatically cache the return value. - It also does not automatically fire any change events. You must manually notify - any changes if you want to observe this property. - - Dependency keys have no effect on volatile properties as they are for cache - invalidation and notification when cached value is invalidated. - - Example: - - ```javascript - import { computed } from '@ember/object'; - - class CallCounter { - _calledCount = 0; - - @computed().volatile() - get calledCount() { - return this._calledCount++; - } - } - ``` - - Classic Class Example: - - ```javascript - import EmberObject, { computed } from '@ember/object'; - - let CallCounter = EmberObject.extend({ - _calledCount: 0, - - value: computed(function() { - return this._calledCount++; - }).volatile() - }); - ``` - @method volatile - @deprecated - @return {ComputedProperty} this - @chainable - @public - */ - volatile(): void { - deprecate( - 'Setting a computed property as volatile has been deprecated. Instead, consider using a native getter with native class syntax.', - false, - { - id: 'computed-property.volatile', - until: '4.0.0', - url: 'https://emberjs.com/deprecations/v3.x#toc_computed-property-volatile', - } - ); - - this._volatile = true; - } - - /** - Call on a computed property to set it into read-only mode. When in this - mode the computed property will throw an error when set. - - Example: - - ```javascript - import { computed, set } from '@ember/object'; - - class Person { - @computed().readOnly() - get guid() { - return 'guid-guid-guid'; - } - } - - let person = new Person(); - set(person, 'guid', 'new-guid'); // will throw an exception - ``` - - Classic Class Example: - - ```javascript - import EmberObject, { computed } from '@ember/object'; - - let Person = EmberObject.extend({ - guid: computed(function() { - return 'guid-guid-guid'; - }).readOnly() - }); - - let person = Person.create(); - person.set('guid', 'new-guid'); // will throw an exception - ``` - - @method readOnly - @return {ComputedProperty} this - @chainable - @public - */ - readOnly(): void { - this._readOnly = true; - assert( - 'Computed properties that define a setter using the new syntax cannot be read-only', - !(this._readOnly && this._setter && this._setter !== this._getter) - ); - } - - /** - Sets the dependent keys on this computed property. Pass any number of - arguments containing key paths that this computed property depends on. - - Example: - - ```javascript - import EmberObject, { computed } from '@ember/object'; - - class President { - constructor(firstName, lastName) { - set(this, 'firstName', firstName); - set(this, 'lastName', lastName); - } - - // Tell Ember that this computed property depends on firstName - // and lastName - @computed().property('firstName', 'lastName') - get fullName() { - return `${this.firstName} ${this.lastName}`; - } - } - - let president = new President('Barack', 'Obama'); - - president.fullName; // 'Barack Obama' - ``` - - Classic Class Example: - - ```javascript - import EmberObject, { computed } from '@ember/object'; - - let President = EmberObject.extend({ - fullName: computed(function() { - return this.get('firstName') + ' ' + this.get('lastName'); - - // Tell Ember that this computed property depends on firstName - // and lastName - }).property('firstName', 'lastName') - }); - - let president = President.create({ - firstName: 'Barack', - lastName: 'Obama' - }); - - president.get('fullName'); // 'Barack Obama' - ``` - - @method property - @deprecated - @param {String} path* zero or more property paths - @return {ComputedProperty} this - @chainable - @public - */ - property(...passedArgs: string[]): void { - deprecate( - 'Setting dependency keys using the `.property()` modifier has been deprecated. Pass the dependency keys directly to computed as arguments instead. If you are using `.property()` on a computed property macro, consider refactoring your macro to receive additional dependent keys in its initial declaration.', - false, - { - id: 'computed-property.property', - until: '4.0.0', - url: 'https://emberjs.com/deprecations/v3.x#toc_computed-property-property', - } - ); - - this._property(...passedArgs); - } - _property(...passedArgs: string[]): void { let args: string[] = []; @@ -549,53 +373,6 @@ export class ComputedProperty extends ComputedDescriptor { this._dependentKeys = args; } - /** - In some cases, you may want to annotate computed properties with additional - metadata about how they function or what values they operate on. For example, - computed property functions may close over variables that are then no longer - available for introspection. You can pass a hash of these values to a - computed property. - - Example: - - ```javascript - import { computed } from '@ember/object'; - import Person from 'my-app/utils/person'; - - class Store { - @computed().meta({ type: Person }) - get person() { - let personId = this.personId; - return Person.create({ id: personId }); - } - } - ``` - - Classic Class Example: - - ```javascript - import { computed } from '@ember/object'; - import Person from 'my-app/utils/person'; - - const Store = EmberObject.extend({ - person: computed(function() { - let personId = this.get('personId'); - return Person.create({ id: personId }); - }).meta({ type: Person }) - }); - ``` - - The hash that you pass to the `meta()` function will be saved on the - computed property descriptor under the `_meta` key. Ember runtime - exposes a public API for retrieving these values from classes, - via the `metaForProperty()` function. - - @method meta - @param {Object} meta - @chainable - @public - */ - get(obj: object, keyName: string): any { if (this._volatile) { return this._getter!.call(obj, keyName); @@ -835,21 +612,230 @@ export type ComputedDecorator = Decorator & PropertyDecorator & ComputedDecorato // TODO: This class can be svelted once `meta` has been deprecated class ComputedDecoratorImpl extends Function { + /** + Call on a computed property to set it into read-only mode. When in this + mode the computed property will throw an error when set. + + Example: + + ```javascript + import { computed, set } from '@ember/object'; + + class Person { + @computed().readOnly() + get guid() { + return 'guid-guid-guid'; + } + } + + let person = new Person(); + set(person, 'guid', 'new-guid'); // will throw an exception + ``` + + Classic Class Example: + + ```javascript + import EmberObject, { computed } from '@ember/object'; + + let Person = EmberObject.extend({ + guid: computed(function() { + return 'guid-guid-guid'; + }).readOnly() + }); + + let person = Person.create(); + person.set('guid', 'new-guid'); // will throw an exception + ``` + + @method readOnly + @return {ComputedProperty} this + @chainable + @public + */ readOnly(this: Decorator) { - (descriptorForDecorator(this) as ComputedProperty).readOnly(); + let desc = descriptorForDecorator(this) as ComputedProperty; + assert( + 'Computed properties that define a setter using the new syntax cannot be read-only', + !(desc._setter && desc._setter !== desc._getter) + ); + desc._readOnly = true; return this; } + /** + Call on a computed property to set it into non-cached mode. When in this + mode the computed property will not automatically cache the return value. + It also does not automatically fire any change events. You must manually notify + any changes if you want to observe this property. + + Dependency keys have no effect on volatile properties as they are for cache + invalidation and notification when cached value is invalidated. + + Example: + + ```javascript + import { computed } from '@ember/object'; + + class CallCounter { + _calledCount = 0; + + @computed().volatile() + get calledCount() { + return this._calledCount++; + } + } + ``` + + Classic Class Example: + + ```javascript + import EmberObject, { computed } from '@ember/object'; + + let CallCounter = EmberObject.extend({ + _calledCount: 0, + + value: computed(function() { + return this._calledCount++; + }).volatile() + }); + ``` + @method volatile + @deprecated + @return {ComputedProperty} this + @chainable + @public + */ volatile(this: Decorator) { - (descriptorForDecorator(this) as ComputedProperty).volatile(); + deprecate( + 'Setting a computed property as volatile has been deprecated. Instead, consider using a native getter with native class syntax.', + false, + { + id: 'computed-property.volatile', + until: '4.0.0', + url: 'https://emberjs.com/deprecations/v3.x#toc_computed-property-volatile', + } + ); + (descriptorForDecorator(this) as ComputedProperty)._volatile = true; return this; } + /** + Sets the dependent keys on this computed property. Pass any number of + arguments containing key paths that this computed property depends on. + + Example: + + ```javascript + import EmberObject, { computed } from '@ember/object'; + + class President { + constructor(firstName, lastName) { + set(this, 'firstName', firstName); + set(this, 'lastName', lastName); + } + + // Tell Ember that this computed property depends on firstName + // and lastName + @computed().property('firstName', 'lastName') + get fullName() { + return `${this.firstName} ${this.lastName}`; + } + } + + let president = new President('Barack', 'Obama'); + + president.fullName; // 'Barack Obama' + ``` + + Classic Class Example: + + ```javascript + import EmberObject, { computed } from '@ember/object'; + + let President = EmberObject.extend({ + fullName: computed(function() { + return this.get('firstName') + ' ' + this.get('lastName'); + + // Tell Ember that this computed property depends on firstName + // and lastName + }).property('firstName', 'lastName') + }); + + let president = President.create({ + firstName: 'Barack', + lastName: 'Obama' + }); + + president.get('fullName'); // 'Barack Obama' + ``` + + @method property + @deprecated + @param {String} path* zero or more property paths + @return {ComputedProperty} this + @chainable + @public + */ property(this: Decorator, ...keys: string[]) { - (descriptorForDecorator(this) as ComputedProperty).property(...keys); + deprecate( + 'Setting dependency keys using the `.property()` modifier has been deprecated. Pass the dependency keys directly to computed as arguments instead. If you are using `.property()` on a computed property macro, consider refactoring your macro to receive additional dependent keys in its initial declaration.', + false, + { + id: 'computed-property.property', + until: '4.0.0', + url: 'https://emberjs.com/deprecations/v3.x#toc_computed-property-property', + } + ); + (descriptorForDecorator(this) as ComputedProperty)._property(...keys); return this; } + /** + In some cases, you may want to annotate computed properties with additional + metadata about how they function or what values they operate on. For example, + computed property functions may close over variables that are then no longer + available for introspection. You can pass a hash of these values to a + computed property. + + Example: + + ```javascript + import { computed } from '@ember/object'; + import Person from 'my-app/utils/person'; + + class Store { + @computed().meta({ type: Person }) + get person() { + let personId = this.personId; + return Person.create({ id: personId }); + } + } + ``` + + Classic Class Example: + + ```javascript + import { computed } from '@ember/object'; + import Person from 'my-app/utils/person'; + + const Store = EmberObject.extend({ + person: computed(function() { + let personId = this.get('personId'); + return Person.create({ id: personId }); + }).meta({ type: Person }) + }); + ``` + + The hash that you pass to the `meta()` function will be saved on the + computed property descriptor under the `_meta` key. Ember runtime + exposes a public API for retrieving these values from classes, + via the `metaForProperty()` function. + + @method meta + @param {Object} meta + @chainable + @public + */ meta(this: Decorator, meta?: any): any { let prop = descriptorForDecorator(this) as ComputedProperty; @@ -863,12 +849,12 @@ class ComputedDecoratorImpl extends Function { // TODO: Remove this when we can provide alternatives in the ecosystem to // addons such as ember-macro-helpers that use it. - get _getter(this: Decorator) { + get _getter() { return (descriptorForDecorator(this) as ComputedProperty)._getter; } // TODO: Refactor this, this is an internal API only - set enumerable(this: Decorator, value: boolean) { + set enumerable(value: boolean) { (descriptorForDecorator(this) as ComputedProperty).enumerable = value; } } diff --git a/packages/@ember/-internals/metal/lib/descriptor_map.ts b/packages/@ember/-internals/metal/lib/descriptor_map.ts index e07ab4d8b04..08f7526092a 100644 --- a/packages/@ember/-internals/metal/lib/descriptor_map.ts +++ b/packages/@ember/-internals/metal/lib/descriptor_map.ts @@ -1,10 +1,7 @@ import { Meta, peekMeta } from '@ember/-internals/meta'; import { assert } from '@ember/debug'; -const DECORATOR_DESCRIPTOR_MAP: WeakMap< - import('./decorator').Decorator, - import('./decorator').ComputedDescriptor | boolean -> = new WeakMap(); +const DECORATOR_DESCRIPTOR_MAP: WeakMap = new WeakMap(); /** Returns the CP descriptor associated with `obj` and `keyName`, if any. @@ -30,8 +27,8 @@ export function descriptorForProperty(obj: object, keyName: string, _meta?: Meta } } -export function descriptorForDecorator(dec: import('./decorator').Decorator) { - return DECORATOR_DESCRIPTOR_MAP.get(dec); +export function descriptorForDecorator(dec: Function): T | true | undefined { + return DECORATOR_DESCRIPTOR_MAP.get(dec) as T; } /** @@ -43,7 +40,7 @@ export function descriptorForDecorator(dec: import('./decorator').Decorator) { @private */ export function isClassicDecorator(dec: any) { - return dec !== null && dec !== undefined && DECORATOR_DESCRIPTOR_MAP.has(dec); + return typeof dec === 'function' && DECORATOR_DESCRIPTOR_MAP.has(dec); } /** @@ -53,6 +50,6 @@ export function isClassicDecorator(dec: any) { @param {function} decorator the value to mark as a decorator @private */ -export function setClassicDecorator(dec: import('./decorator').Decorator, value: any = true) { +export function setClassicDecorator(dec: Function, value: any = true) { DECORATOR_DESCRIPTOR_MAP.set(dec, value); } diff --git a/packages/@ember/-internals/metal/lib/mixin.ts b/packages/@ember/-internals/metal/lib/mixin.ts index f4eccd50cc8..86561d70522 100644 --- a/packages/@ember/-internals/metal/lib/mixin.ts +++ b/packages/@ember/-internals/metal/lib/mixin.ts @@ -4,18 +4,16 @@ import { ENV } from '@ember/-internals/environment'; import { Meta, meta as metaFor, peekMeta } from '@ember/-internals/meta'; import { - getListeners, - getObservers, - getOwnPropertyDescriptors, guidFor, makeArray, + observerListenerMetaFor, ROOT, setObservers, wrap, } from '@ember/-internals/utils'; import { assert, deprecate } from '@ember/debug'; import { ALIAS_METHOD } from '@ember/deprecated-features'; -import { assign } from '@ember/polyfills'; +import { _WeakSet, assign } from '@ember/polyfills'; import { DEBUG } from '@glimmer/env'; import { ComputedDecorator, @@ -24,77 +22,33 @@ import { ComputedPropertySetter, } from './computed'; import { makeComputedDecorator, nativeDescDecorator } from './decorator'; -import { - descriptorForDecorator, - descriptorForProperty, - isClassicDecorator, -} from './descriptor_map'; +import { descriptorForDecorator, descriptorForProperty } from './descriptor_map'; import { addListener, removeListener } from './events'; import expandProperties from './expand_properties'; import { classToString, setUnprocessedMixins } from './namespace_search'; -import { addObserver, removeObserver } from './observer'; -import { defineProperty } from './properties'; +import { addObserver, removeObserver, revalidateObservers } from './observer'; +import { defineDecorator, defineValue } from './properties'; const a_concat = Array.prototype.concat; const { isArray } = Array; -function isMethod(obj: any): boolean { - return ( - 'function' === typeof obj && - obj.isMethod !== false && - obj !== Boolean && - obj !== Object && - obj !== Number && - obj !== Array && - obj !== Date && - obj !== String - ); -} - -function isAccessor(desc: PropertyDescriptor) { - return typeof desc.get === 'function' || typeof desc.set === 'function'; -} - function extractAccessors(properties: { [key: string]: any } | undefined) { if (properties !== undefined) { - let descriptors = getOwnPropertyDescriptors(properties); - let keys = Object.keys(descriptors); - let hasAccessors = keys.some(key => isAccessor(descriptors[key])); - - if (hasAccessors) { - let extracted = {}; - - keys.forEach(key => { - let descriptor = descriptors[key]; + let keys = Object.keys(properties); - if (isAccessor(descriptor)) { - extracted[key] = nativeDescDecorator(descriptor); - } else { - extracted[key] = properties[key]; - } - }); + for (let i = 0; i < keys.length; i++) { + let key = keys[i]; + let desc = Object.getOwnPropertyDescriptor(properties, key)!; - return extracted; + if (desc.get !== undefined || desc.set !== undefined) { + Object.defineProperty(properties, key, { value: nativeDescDecorator(desc) }); + } } } return properties; } -const CONTINUE: MixinLike = {}; - -function mixinProperties(mixinsMeta: Meta, mixin: T): MixinLike { - if (mixin instanceof Mixin) { - if (mixinsMeta.hasMixin(mixin)) { - return CONTINUE; - } - mixinsMeta.addMixin(mixin); - return mixin.properties!; - } else { - return mixin; // apply anonymous mixin properties - } -} - function concatenatedMixinProperties( concatProp: string, props: { [key: string]: any }, @@ -110,62 +64,75 @@ function concatenatedMixinProperties( } function giveDecoratorSuper( - meta: Meta, key: string, decorator: ComputedDecorator, - values: { [key: string]: any }, - descs: { [key: string]: any }, - base: object + property: ComputedProperty | true, + descs: { [key: string]: any } ): ComputedDecorator { - let property = descriptorForDecorator(decorator)!; - let superProperty; - - if (!(property instanceof ComputedProperty) || property._getter === undefined) { + if (property === true) { return decorator; } - // Computed properties override methods, and do not call super to them - if (values[key] === undefined) { - // Find the original descriptor in a parent mixin - superProperty = descriptorForDecorator(descs[key]); + let originalGetter = property._getter; + + if (originalGetter === undefined) { + return decorator; } - // If we didn't find the original descriptor in a parent mixin, find - // it on the original object. - if (!superProperty) { - superProperty = descriptorForProperty(base, key, meta); + let superDesc = descs[key]; + + // Check to see if the super property is a decorator first, if so load its descriptor + let superProperty: ComputedProperty | true | undefined = + typeof superDesc === 'function' ? descriptorForDecorator(superDesc) : superDesc; + + if (superProperty === undefined || superProperty === true) { + return decorator; } - if (superProperty === undefined || !(superProperty instanceof ComputedProperty)) { + let superGetter = superProperty._getter; + + if (superGetter === undefined) { return decorator; } - let get = wrap(property._getter!, superProperty._getter!) as ComputedPropertyGetter; + let get = wrap(originalGetter, superGetter) as ComputedPropertyGetter; let set; + let originalSetter = property._setter; + let superSetter = superProperty._setter; - if (superProperty._setter) { - if (property._setter) { - set = wrap(property._setter, superProperty._setter) as ComputedPropertySetter; + if (superSetter !== undefined) { + if (originalSetter !== undefined) { + set = wrap(originalSetter, superSetter) as ComputedPropertySetter; } else { // If the super property has a setter, we default to using it no matter what. // This is clearly very broken and weird, but it's what was here so we have // to keep it until the next major at least. // // TODO: Add a deprecation here. - set = superProperty._setter; + set = superSetter; } } else { - set = property._setter; + set = originalSetter; } // only create a new CP if we must - if (get !== property._getter || set !== property._setter) { + if (get !== originalGetter || set !== originalSetter) { // Since multiple mixins may inherit from the same parent, we need // to clone the computed property so that other mixins do not receive // the wrapped version. - let newProperty = Object.create(property); - newProperty._getter = get; - newProperty._setter = set; + let dependentKeys = property._dependentKeys || []; + let newProperty = new ComputedProperty([ + ...dependentKeys, + { + get, + set, + }, + ]); + + newProperty._readOnly = property._readOnly; + newProperty._volatile = property._volatile; + newProperty._meta = property._meta; + newProperty.enumerable = property.enumerable; return makeComputedDecorator(newProperty, ComputedProperty) as ComputedDecorator; } @@ -174,7 +141,6 @@ function giveDecoratorSuper( } function giveMethodSuper( - obj: object, key: string, method: Function, values: { [key: string]: any }, @@ -188,12 +154,6 @@ function giveMethodSuper( // Find the original method in a parent mixin let superMethod = values[key]; - // If we didn't find the original value in a parent mixin, find it in - // the original object - if (superMethod === undefined && descriptorForProperty(obj, key) === undefined) { - superMethod = obj[key]; - } - // Only wrap the new method if the original method was a function if (typeof superMethod === 'function') { return wrap(method, superMethod); @@ -202,13 +162,8 @@ function giveMethodSuper( return method; } -function applyConcatenatedProperties( - obj: any, - key: string, - value: any, - values: { [key: string]: any } -) { - let baseValue = values[key] || obj[key]; +function applyConcatenatedProperties(key: string, value: any, values: { [key: string]: any }) { + let baseValue = values[key]; let ret = makeArray(baseValue).concat(makeArray(value)); if (DEBUG) { @@ -224,12 +179,11 @@ function applyConcatenatedProperties( } function applyMergedProperties( - obj: { [key: string]: any }, key: string, value: { [key: string]: any }, values: { [key: string]: any } ): { [key: string]: any } { - let baseValue = values[key] || obj[key]; + let baseValue = values[key]; assert( `You passed in \`${JSON.stringify( @@ -245,16 +199,15 @@ function applyMergedProperties( let newBase = assign({}, baseValue); let hasFunction = false; - for (let prop in value) { - if (!Object.prototype.hasOwnProperty.call(value, prop)) { - continue; - } + let props = Object.keys(value); + for (let i = 0; i < props.length; i++) { + let prop = props[i]; let propValue = value[prop]; - if (isMethod(propValue)) { - // TODO: support for Computed Properties, etc? + + if (typeof propValue === 'function') { hasFunction = true; - newBase[prop] = giveMethodSuper(obj, prop, propValue, baseValue, {}); + newBase[prop] = giveMethodSuper(prop, propValue, baseValue, {}); } else { newBase[prop] = propValue; } @@ -267,56 +220,16 @@ function applyMergedProperties( return newBase; } -function addNormalizedProperty( - base: any, - key: string, - value: any, - meta: Meta, - descs: { [key: string]: any }, - values: { [key: string]: any }, - concats?: string[], - mergings?: string[] -): void { - if (isClassicDecorator(value)) { - // Wrap descriptor function to implement _super() if needed - descs[key] = giveDecoratorSuper(meta, key, value, values, descs, base); - values[key] = undefined; - } else { - if ( - (concats && concats.indexOf(key) >= 0) || - key === 'concatenatedProperties' || - key === 'mergedProperties' - ) { - value = applyConcatenatedProperties(base, key, value, values); - } else if (mergings && mergings.indexOf(key) > -1) { - value = applyMergedProperties(base, key, value, values); - } else if (isMethod(value)) { - value = giveMethodSuper(base, key, value, values, descs); - } - - descs[key] = undefined; - values[key] = value; - } -} - -interface HasWillMergeMixin { - willMergeMixin?: (props: MixinLike) => void; -} - function mergeMixins( mixins: MixinLike[], meta: Meta, descs: { [key: string]: object }, values: { [key: string]: object }, base: { [key: string]: object }, - keys: string[] + keys: string[], + keysWithSuper: string[] ): void { - let currentMixin, props, key, concats, mergings; - - function removeKeys(keyName: string) { - delete descs[keyName]; - delete values[keyName]; - } + let currentMixin; for (let i = 0; i < mixins.length; i++) { currentMixin = mixins[i]; @@ -327,37 +240,109 @@ function mergeMixins( Object.prototype.toString.call(currentMixin) !== '[object Array]' ); - props = mixinProperties(meta, currentMixin); - if (props === CONTINUE) { - continue; - } - - if (props) { - // remove willMergeMixin after 3.4 as it was used for _actions - if ((base as HasWillMergeMixin).willMergeMixin) { - (base as HasWillMergeMixin).willMergeMixin!(props); + if (MIXINS.has(currentMixin)) { + if (meta.hasMixin(currentMixin)) { + continue; } - concats = concatenatedMixinProperties('concatenatedProperties', props, values, base); - mergings = concatenatedMixinProperties('mergedProperties', props, values, base); + meta.addMixin(currentMixin); + + let { properties, mixins } = currentMixin; + + if (properties !== undefined) { + mergeProps(meta, properties, descs, values, base, keys, keysWithSuper); + } else if (mixins !== undefined) { + mergeMixins(mixins, meta, descs, values, base, keys, keysWithSuper); + + if (currentMixin._without !== undefined) { + currentMixin._without.forEach((keyName: string) => { + // deleting the key means we won't process the value + let index = keys.indexOf(keyName); - for (key in props) { - if (!Object.prototype.hasOwnProperty.call(props, key)) { - continue; + if (index !== -1) { + keys.splice(index, 1); + } + }); } - keys.push(key); - addNormalizedProperty(base, key, props[key], meta, descs, values, concats, mergings); } + } else { + mergeProps(meta, currentMixin, descs, values, base, keys, keysWithSuper); + } + } +} + +function mergeProps( + meta: Meta, + props: { [key: string]: unknown }, + descs: { [key: string]: unknown }, + values: { [key: string]: unknown }, + base: { [key: string]: unknown }, + keys: string[], + keysWithSuper: string[] +) { + let concats = concatenatedMixinProperties('concatenatedProperties', props, values, base); + let mergings = concatenatedMixinProperties('mergedProperties', props, values, base); + + let propKeys = Object.keys(props); + + for (let i = 0; i < propKeys.length; i++) { + let key = propKeys[i]; + let value = props[key]; + + if (value === undefined) continue; + + if (keys.indexOf(key) === -1) { + keys.push(key); + + let desc = meta.peekDescriptors(key); - // manually copy toString() because some JS engines do not enumerate it - if (Object.prototype.hasOwnProperty.call(props, 'toString')) { - base.toString = props.toString; + if (desc === undefined) { + // The superclass did not have a CP, which means it may have + // observers or listeners on that property. + let prev = (values[key] = base[key]); + + if (typeof prev === 'function') { + updateObserversAndListeners(base, key, prev, false); + } + } else { + descs[key] = desc; + + // The super desc will be overwritten on descs, so save off the fact that + // there was a super so we know to Object.defineProperty when writing + // the value + keysWithSuper.push(key); + + desc.teardown(base, key, meta); } - } else if (currentMixin.mixins) { - mergeMixins(currentMixin.mixins, meta, descs, values, base, keys); - if (currentMixin._without) { - currentMixin._without.forEach(removeKeys); + } + + let isFunction = typeof value === 'function'; + + if (isFunction) { + let desc: ComputedProperty | undefined | true = descriptorForDecorator(value as Function); + + if (desc !== undefined) { + // Wrap descriptor function to implement _super() if needed + descs[key] = giveDecoratorSuper(key, value as ComputedDecorator, desc, descs); + values[key] = undefined; + + continue; } } + + if ( + (concats && concats.indexOf(key) >= 0) || + key === 'concatenatedProperties' || + key === 'mergedProperties' + ) { + value = applyConcatenatedProperties(key, value, values); + } else if (mergings && mergings.indexOf(key) > -1) { + value = applyMergedProperties(key, value as object, values); + } else if (isFunction) { + value = giveMethodSuper(key, value as Function, values, descs); + } + + values[key] = value; + descs[key] = undefined; } } @@ -395,8 +380,11 @@ if (ALIAS_METHOD) { } function updateObserversAndListeners(obj: object, key: string, fn: Function, add: boolean) { - let observers = getObservers(fn); - let listeners = getListeners(fn); + let meta = observerListenerMetaFor(fn); + + if (meta === undefined) return; + + let { observers, listeners } = meta; if (observers !== undefined) { let updateObserver = add ? addObserver : removeObserver; @@ -415,27 +403,12 @@ function updateObserversAndListeners(obj: object, key: string, fn: Function, add } } -function replaceObserversAndListeners( - obj: object, - key: string, - prev: Function | null, - next: Function | null -): void { - if (typeof prev === 'function') { - updateObserversAndListeners(obj, key, prev, false); - } - - if (typeof next === 'function') { - updateObserversAndListeners(obj, key, next, true); - } -} - -export function applyMixin(obj: { [key: string]: any }, mixins: Mixin[]) { - let descs = {}; - let values = {}; +export function applyMixin(obj: { [key: string]: any }, mixins: Mixin[], _hideKeys = false) { + let descs = Object.create(null); + let values = Object.create(null); let meta = metaFor(obj); let keys: string[] = []; - let key, value, desc; + let keysWithSuper: string[] = []; (obj as any)._super = ROOT; @@ -446,36 +419,40 @@ export function applyMixin(obj: { [key: string]: any }, mixins: Mixin[]) { // * Set up _super wrapping if necessary // * Set up computed property descriptors // * Copying `toString` in broken browsers - mergeMixins(mixins, meta, descs, values, obj, keys); + mergeMixins(mixins, meta, descs, values, obj, keys, keysWithSuper); - for (let i = 0; i < keys.length; i++) { - key = keys[i]; - if (key === 'constructor' || !Object.prototype.hasOwnProperty.call(values, key)) { - continue; - } + let hasDidDefineProperty = typeof obj.didDefineProperty === 'function'; - desc = descs[key]; - value = values[key]; + for (let i = 0; i < keys.length; i++) { + let key = keys[i]; + let value = values[key]; + let desc = descs[key]; if (ALIAS_METHOD) { - while (value && value instanceof AliasImpl) { + while (value !== undefined && isAlias(value)) { let followed = followMethodAlias(obj, value, descs, values); desc = followed.desc; value = followed.value; } } - if (desc === undefined && value === undefined) { - continue; + if (value !== undefined) { + if (typeof value === 'function') { + updateObserversAndListeners(obj, key, value, true); + } + + defineValue(obj, key, value, keysWithSuper.indexOf(key) !== -1, !_hideKeys); + } else if (desc !== undefined) { + defineDecorator(obj, key, desc, meta); } - if (descriptorForProperty(obj, key) !== undefined) { - replaceObserversAndListeners(obj, key, null, value); - } else { - replaceObserversAndListeners(obj, key, obj[key], value); + if (hasDidDefineProperty === true) { + obj.didDefineProperty!(obj, key, value); } + } - defineProperty(obj, key, desc, value, meta); + if (!meta.isPrototypeMeta(obj)) { + revalidateObservers(obj); } return obj; @@ -493,6 +470,8 @@ export function mixin(obj: object, ...args: any[]) { return obj; } +const MIXINS = new _WeakSet(); + /** The `Mixin` class allows you to create mixins, whose properties can be added to other classes. For instance, @@ -582,6 +561,7 @@ export default class Mixin { _without: any[] | undefined; constructor(mixins: Mixin[] | undefined, properties?: { [key: string]: any }) { + MIXINS.add(this); this.properties = extractAccessors(properties); this.mixins = buildMixinsArray(mixins); this.ownerConstructor = undefined; @@ -608,7 +588,6 @@ export default class Mixin { @public */ static create(...args: any[]): Mixin { - // ES6TODO: this relies on a global state? setUnprocessedMixins(); let M = this; return new M(args, undefined); @@ -661,8 +640,13 @@ export default class Mixin { @return applied object @private */ - apply(obj: object) { - return applyMixin(obj, [this]); + apply(obj: object, _hideKeys = false) { + // Ember.NativeArray is a normal Ember.Mixin that we mix into `Array.prototype` when prototype extensions are enabled + // mutating a native object prototype like this should _not_ result in enumerable properties being added (or we have significant + // issues with things like deep equality checks from test frameworks, or things like jQuery.extend(true, [], [])). + // + // _hideKeys disables enumerablity when applying the mixin. This is a hack, and we should stop mutating the array prototype by default 😫 + return applyMixin(obj, [this], _hideKeys); } applyPartial(obj: object) { @@ -679,7 +663,7 @@ export default class Mixin { if (typeof obj !== 'object' || obj === null) { return false; } - if (obj instanceof Mixin) { + if (MIXINS.has(obj)) { return _detect(obj, this); } let meta = peekMeta(obj); @@ -719,8 +703,8 @@ function buildMixinsArray(mixins: MixinLike[] | undefined): Mixin[] | undefined Object.prototype.toString.call(x) !== '[object Array]' ); - if (x instanceof Mixin) { - m[i] = x; + if (MIXINS.has(x)) { + m[i] = x as Mixin; } else { m[i] = new Mixin(undefined, x); } @@ -780,9 +764,19 @@ declare class Alias { let AliasImpl: typeof Alias; +let isAlias: (alias: any) => alias is Alias; + if (ALIAS_METHOD) { + const ALIASES = new _WeakSet(); + + isAlias = (alias: any): alias is Alias => { + return ALIASES.has(alias); + }; + AliasImpl = class AliasImpl { - constructor(public methodName: string) {} + constructor(public methodName: string) { + ALIASES.add(this); + } } as typeof Alias; } @@ -896,10 +890,9 @@ export function observer(...args: (string | Function | ObserverDefinition)[]) { assert('observer called without sync', typeof sync === 'boolean'); let paths: string[] = []; - let addWatchedProperty = (path: string) => paths.push(path); for (let i = 0; i < dependentKeys.length; ++i) { - expandProperties(dependentKeys[i] as string, addWatchedProperty); + expandProperties(dependentKeys[i] as string, (path: string) => paths.push(path)); } setObservers(func as Function, { diff --git a/packages/@ember/-internals/metal/lib/properties.ts b/packages/@ember/-internals/metal/lib/properties.ts index fd47c50a769..7840166d183 100644 --- a/packages/@ember/-internals/metal/lib/properties.ts +++ b/packages/@ember/-internals/metal/lib/properties.ts @@ -66,11 +66,9 @@ export function defineProperty( keyName: string, desc?: Decorator | undefined | null, data?: any | undefined | null, - meta?: Meta + _meta?: Meta ): void { - if (meta === undefined) { - meta = metaFor(obj); - } + let meta = _meta === undefined ? metaFor(obj) : _meta; let previousDesc = descriptorForProperty(obj, keyName, meta); let wasDescriptor = previousDesc !== undefined; @@ -78,49 +76,11 @@ export function defineProperty( previousDesc.teardown(obj, keyName, meta); } - // used to track if the the property being defined be enumerable - let enumerable = true; - - // Ember.NativeArray is a normal Ember.Mixin that we mix into `Array.prototype` when prototype extensions are enabled - // mutating a native object prototype like this should _not_ result in enumerable properties being added (or we have significant - // issues with things like deep equality checks from test frameworks, or things like jQuery.extend(true, [], [])). - // - // this is a hack, and we should stop mutating the array prototype by default 😫 - if (obj === Array.prototype) { - enumerable = false; - } - let value; if (isClassicDecorator(desc)) { - let propertyDesc; - - if (DEBUG) { - propertyDesc = desc!(obj, keyName, undefined, meta, true); - } else { - propertyDesc = desc!(obj, keyName, undefined, meta); - } - - Object.defineProperty(obj, keyName, propertyDesc as PropertyDescriptor); - - // pass the decorator function forward for backwards compat - value = desc; - } else if (desc === undefined || desc === null) { - value = data; - - if (wasDescriptor || enumerable === false) { - Object.defineProperty(obj, keyName, { - configurable: true, - enumerable, - writable: true, - value, - }); - } else { - if (DEBUG) { - setWithMandatorySetter!(obj, keyName, data); - } else { - obj[keyName] = data; - } - } + value = defineDecorator(obj, keyName, desc!, meta); + } else if (desc === null || desc === undefined) { + value = defineValue(obj, keyName, data, wasDescriptor, true); } else { value = desc; @@ -140,3 +100,43 @@ export function defineProperty( (obj as ExtendedObject).didDefineProperty!(obj, keyName, value); } } + +export function defineDecorator(obj: object, keyName: string, desc: Decorator, meta: Meta) { + let propertyDesc; + + if (DEBUG) { + propertyDesc = desc!(obj, keyName, undefined, meta, true); + } else { + propertyDesc = desc!(obj, keyName, undefined, meta); + } + + Object.defineProperty(obj, keyName, propertyDesc as PropertyDescriptor); + + // pass the decorator function forward for backwards compat + return desc; +} + +export function defineValue( + obj: object, + keyName: string, + value: unknown, + wasDescriptor: boolean, + enumerable = true +) { + if (wasDescriptor === true || enumerable === false) { + Object.defineProperty(obj, keyName, { + configurable: true, + enumerable, + writable: true, + value, + }); + } else { + if (DEBUG) { + setWithMandatorySetter!(obj, keyName, value); + } else { + obj[keyName] = value; + } + } + + return value; +} diff --git a/packages/@ember/-internals/metal/lib/tags.ts b/packages/@ember/-internals/metal/lib/tags.ts index d19d39087c6..50bc82757ab 100644 --- a/packages/@ember/-internals/metal/lib/tags.ts +++ b/packages/@ember/-internals/metal/lib/tags.ts @@ -9,7 +9,7 @@ import { CONSTANT_TAG, dirtyTagFor, Tag, tagFor, TagMeta } from '@glimmer/valida export const CUSTOM_TAG_FOR = symbol('CUSTOM_TAG_FOR'); // This is exported for `@tracked`, but should otherwise be avoided. Use `tagForObject`. -export const SELF_TAG: string = symbol('SELF_TAG'); +export const SELF_TAG = symbol('SELF_TAG'); export function tagForProperty( obj: object, diff --git a/packages/@ember/-internals/runtime/lib/mixins/array.js b/packages/@ember/-internals/runtime/lib/mixins/array.js index 09d9e4de331..f5baade158f 100644 --- a/packages/@ember/-internals/runtime/lib/mixins/array.js +++ b/packages/@ember/-internals/runtime/lib/mixins/array.js @@ -1982,7 +1982,7 @@ NativeArray = NativeArray.without(...ignore); let A; if (ENV.EXTEND_PROTOTYPES.Array) { - NativeArray.apply(Array.prototype); + NativeArray.apply(Array.prototype, true); A = function(arr) { assert( diff --git a/packages/@ember/-internals/utils/index.ts b/packages/@ember/-internals/utils/index.ts index 95427e74017..dcbfdc1e87d 100644 --- a/packages/@ember/-internals/utils/index.ts +++ b/packages/@ember/-internals/utils/index.ts @@ -11,15 +11,13 @@ export { default as symbol, isInternalSymbol } from './lib/symbol'; export { default as dictionary } from './lib/dictionary'; export { default as getDebugName } from './lib/get-debug-name'; -export { default as getOwnPropertyDescriptors } from './lib/get-own-property-descriptors'; export { uuid, GUID_KEY, generateGuid, guidFor } from './lib/guid'; export { default as intern } from './lib/intern'; export { checkHasSuper, ROOT, wrap, - getObservers, - getListeners, + observerListenerMetaFor, setObservers, setListeners, } from './lib/super'; diff --git a/packages/@ember/-internals/utils/lib/get-own-property-descriptors.ts b/packages/@ember/-internals/utils/lib/get-own-property-descriptors.ts deleted file mode 100644 index 30ddff39dd5..00000000000 --- a/packages/@ember/-internals/utils/lib/get-own-property-descriptors.ts +++ /dev/null @@ -1,17 +0,0 @@ -let getOwnPropertyDescriptors: (obj: { [x: string]: any }) => { [x: string]: PropertyDescriptor }; - -if (Object.getOwnPropertyDescriptors !== undefined) { - getOwnPropertyDescriptors = Object.getOwnPropertyDescriptors; -} else { - getOwnPropertyDescriptors = function(obj: object) { - let descriptors = {}; - - Object.keys(obj).forEach(key => { - descriptors[key] = Object.getOwnPropertyDescriptor(obj, key); - }); - - return descriptors; - }; -} - -export default getOwnPropertyDescriptors; diff --git a/packages/@ember/-internals/utils/lib/super.ts b/packages/@ember/-internals/utils/lib/super.ts index c143420c116..27314fe1c94 100644 --- a/packages/@ember/-internals/utils/lib/super.ts +++ b/packages/@ember/-internals/utils/lib/super.ts @@ -36,26 +36,36 @@ function hasSuper(func: Function) { return hasSuper; } -const OBSERVERS_MAP = new WeakMap(); - -export function setObservers(func: Function, observers: { paths: string[]; sync: boolean }) { - OBSERVERS_MAP.set(func, observers); +class ObserverListenerMeta { + listeners?: string[] = undefined; + observers?: { paths: string[]; sync: boolean } = undefined; } -export function getObservers(func: Function) { - return OBSERVERS_MAP.get(func); -} +const OBSERVERS_LISTENERS_MAP = new WeakMap(); -const LISTENERS_MAP = new WeakMap(); +function createObserverListenerMetaFor(fn: Function) { + let meta = OBSERVERS_LISTENERS_MAP.get(fn); -export function setListeners(func: Function, listeners?: string[]) { - if (listeners) { - LISTENERS_MAP.set(func, listeners); + if (meta === undefined) { + meta = new ObserverListenerMeta(); + OBSERVERS_LISTENERS_MAP.set(fn, meta); } + + return meta; +} + +export function observerListenerMetaFor(fn: Function) { + return OBSERVERS_LISTENERS_MAP.get(fn); } -export function getListeners(func: Function) { - return LISTENERS_MAP.get(func); +export function setObservers(func: Function, observers: { paths: string[]; sync: boolean }) { + let meta = createObserverListenerMetaFor(func); + meta.observers = observers; +} + +export function setListeners(func: Function, listeners: string[]) { + let meta = createObserverListenerMetaFor(func); + meta.listeners = listeners; } const IS_WRAPPED_FUNCTION_SET = new WeakSet(); @@ -93,8 +103,12 @@ function _wrap(func: Function, superFunc: Function): Function { } IS_WRAPPED_FUNCTION_SET.add(superWrapper); - setObservers(superWrapper, getObservers(func)); - setListeners(superWrapper, getListeners(func)); + + let meta = OBSERVERS_LISTENERS_MAP.get(func); + + if (meta !== undefined) { + OBSERVERS_LISTENERS_MAP.set(superWrapper, meta); + } return superWrapper; }