-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
/
cached.ts
155 lines (124 loc) · 5.02 KB
/
cached.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
// 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
import { DEBUG } from '@glimmer/env';
import { createCache, getValue } from '@glimmer/validator';
const CacheMap = new WeakMap();
/**
* @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
*/
export const cached: PropertyDecorator = (...args: any[]) => {
const [target, key, descriptor] = args;
// Error on `@cached()`, `@cached(...args)`, and `@cached propName = value;`
if (DEBUG && target === undefined) throwCachedExtraneousParens();
if (
DEBUG &&
(typeof target !== 'object' ||
typeof key !== 'string' ||
typeof descriptor !== 'object' ||
args.length !== 3)
) {
throwCachedInvalidArgsError(args);
}
if (DEBUG && (!('get' in descriptor) || typeof descriptor.get !== 'function')) {
throwCachedGetterOnlyError(key);
}
CacheMap.set(target, [...CacheMap.get(target)||[], key]);
const caches = new WeakMap();
const getter = descriptor.get;
descriptor.get = function (): unknown {
if (!caches.has(this)) {
caches.set(this, createCache(getter.bind(this)));
}
return getValue(caches.get(this));
};
};
function throwCachedExtraneousParens(): never {
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: string): never {
throw new Error(`The @cached decorator must be applied to getters. '${key}' is not a getter.`);
}
function throwCachedInvalidArgsError(args: unknown[] = []): never {
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 function isCachedProperty(object: object, prop: string) {
return (CacheMap.get(object) || []).includes(prop);
}