UNPKG

ember-source

Version:

A JavaScript framework for creating ambitious web applications

138 lines (113 loc) 5.21 kB
import '../../@ember/-internals/meta/lib/meta.js'; import '../../shared-chunks/mandatory-setter-DHZe7-kW.js'; import '../../@ember/debug/index.js'; import '../destroyable/index.js'; import { createCache, getValue } from '../validator/index.js'; import '../../shared-chunks/debug-to-string-CFb7h0lY.js'; import '../global-context/index.js'; import '../../shared-chunks/reference-C3TKDRnP.js'; import '../../shared-chunks/capabilities-O_xc7Yqk.js'; export { $ as tracked } from '../../shared-chunks/observers-Bj9qLVau.js'; import '../../@ember/-internals/environment/index.js'; // NOTE: copied from: https://github.com/glimmerjs/glimmer.js/pull/358 // Both glimmerjs/glimmer.js and emberjs/ember.js have the exact same implementation // of @cached, so any changes made to one should also be made to the other /** * @decorator * Gives the getter a caching behavior. The return value of the getter will be cached until any of the properties it is entangled with are invalidated. This is useful when a getter is expensive and used very often. For instance, in this `GuestList` class, we have the `sortedGuests` getter that sorts the guests alphabetically: ```javascript import { tracked } from '@glimmer/tracking'; class GuestList { @tracked guests = ['Zoey', 'Tomster']; get sortedGuests() { return this.guests.slice().sort() } } ``` Every time `sortedGuests` is accessed, a new array will be created and sorted, because JavaScript getters do not cache by default. When the guest list is small, like the one in the example, this is not a problem. However, if the guest list were to grow very large, it would mean that we would be doing a large amount of work each time we accessed `sortedGuests`. With `@cached`, we can cache the value instead: ```javascript import { tracked, cached } from '@glimmer/tracking'; class GuestList { @tracked guests = ['Zoey', 'Tomster']; @cached get sortedGuests() { return this.guests.slice().sort() } } ``` Now the `sortedGuests` getter will be cached based on autotracking. It will only rerun and create a new sorted array when the guests tracked property is updated. ### Tradeoffs Overuse is discouraged. In general, you should avoid using `@cached` unless you have confirmed that the getter you are decorating is computationally expensive, since `@cached` adds a small amount of overhead to the getter. While the individual costs are small, a systematic use of the `@cached` decorator can add up to a large impact overall in your app. Many getters and tracked properties are only accessed once during rendering, and then never rerendered, so adding `@cached` when unnecessary can negatively impact performance. Also, `@cached` may rerun even if the values themselves have not changed, since tracked properties will always invalidate. For example updating an integer value from `5` to an other `5` will trigger a rerun of the cached properties building from this integer. Avoiding a cache invalidation in this case is not something that can be achieved on the `@cached` decorator itself, but rather when updating the underlying tracked values, by applying some diff checking mechanisms: ```javascript if (nextValue !== this.trackedProp) { this.trackedProp = nextValue; } ``` Here equal values won't update the property, therefore not triggering the subsequent cache invalidations of the `@cached` properties who were using this `trackedProp`. Remember that setting tracked data should only be done during initialization, or as the result of a user action. Setting tracked data during render (such as in a getter), is not supported. @method cached @static @for @glimmer/tracking @public */ const cached = (...args) => { const [target, key, descriptor] = args; // Error on `@cached()`, `@cached(...args)`, and `@cached propName = value;` if (target === undefined) throwCachedExtraneousParens(); if ((typeof target !== 'object' || typeof key !== 'string' || typeof descriptor !== 'object' || args.length !== 3)) { throwCachedInvalidArgsError(args); } if ((!('get' in descriptor) || typeof descriptor.get !== 'function')) { throwCachedGetterOnlyError(key); } const caches = new WeakMap(); const getter = descriptor.get; descriptor.get = function () { if (!caches.has(this)) { caches.set(this, createCache(getter.bind(this))); } return getValue(caches.get(this)); }; }; function throwCachedExtraneousParens() { throw new Error('You attempted to use @cached(), which is not necessary nor supported. Remove the parentheses and you will be good to go!'); } function throwCachedGetterOnlyError(key) { throw new Error(`The @cached decorator must be applied to getters. '${key}' is not a getter.`); } function throwCachedInvalidArgsError(args = []) { throw new Error(`You attempted to use @cached on with ${args.length > 1 ? 'arguments' : 'an argument'} ( @cached(${args.map(d => `'${d}'`).join(', ')}), which is not supported. Dependencies are automatically tracked, so you can just use ${'`@cached`'}`); } export { cached };