UNPKG

10.6 kBMarkdownView Raw
1These are notes about the implementation of the 2021-12 decorators transform.
2The implementation's goals are (in descending order):
3
41. Being accurate to the actual proposal (e.g. not defining additional
5 properties unless required, matching semantics exactly, etc.). This includes
6 being able to work properly with private fields and methods.
72. Transpiling to a very minimal and minifiable output. This transform will
8 affect each and every decorated class, so ensuring that the output is not 10x
9 the size of the original is important.
103. Having good runtime performance. Decoration output has the potential to
11 drastically impact startup performance, since it runs whenever a decorated
12 class is defined. In addition, every instance of a decorated class may be
13 impacted for certain types of decorators.
14
15All of these goals come somewhat at the expense of readability and can make the
16implementation difficult to understand, so these notes are meant to document the
17motivations behind the design.
18
19## Overview
20
21Given a simple decorated class like this one:
22
23```js
24@dec
25class Class {
26 @dec a = 123;
27
28 @dec static #b() {
29 console.log('foo');
30 }
31
32 [someVal]() {}
33
34 @dec
35 @dec2
36 accessor #c = 456;
37}
38```
39
40It's output would be something like the following:
41
42```js
43import { applyDecs } from '@babel/helpers';
44
45let _initInstance, _initClass, _initStatic, _init_a, _call_b, _computedKey, _init_c, _get_c, _set_c;
46
47let _dec = dec,
48 _dec2 = dec,
49 _computedKey = someVal,
50 _dec3 = dec,
51 _dec4 = dec2;
52
53let _Class;
54class Class {
55 static {
56 [
57 _init_a,
58 _call_b,
59 _init_c,
60 _get_c,
61 _set_c,
62 _Class,
63 _initClass,
64 _initProto,
65 _initStatic,
66 ] = _applyDecs(Class,
67 [
68 [_dec, 0, "a"],
69 [
70 _dec2,
71 7,
72 "b",
73 function () {
74 console.log('foo');
75 }
76 ],
77 [
78 [_dec4, _dec5],
79 1,
80 "c",
81 function () {
82 return this.#a;
83 },
84 function (value) {
85 this.#a = value;
86 }
87 ]
88 ],
89 [dec]
90 );
91
92 _initStatic(Class);
93 }
94
95 a = (initInstance(this), _init_a(this, 123));
96
97 static #b(...args) {
98 _call_b(this, args);
99 }
100
101 [_computedKey]() {}
102
103 #a = _init_c(this, 123);
104 get #c() {
105 return _get_c(this);
106 }
107 set #c(v) {
108 _set_c(this, v);
109 }
110
111 static {
112 initClass(C);
113 }
114}
115```
116
117Let's break this output down a bit:
118
119```js
120let initInstance, initClass, _init_a, _call_b, _init_c, _get_c, _set_c;
121```
122
123First, we need to setup some local variables outside of the class. These are
124for:
125
126- Decorated class field/accessor initializers
127- Extra initializer functions added by `addInitializers`
128- Private class methods
129
130These are essentially all values that cannot be defined on the class itself via
131`Object.defineProperty`, so we have to insert them into the class manually,
132ahead of time and populate them when we run our decorators.
133
134```js
135let _dec = dec,
136 _dec2 = dec,
137 _computedKey = someVal,
138 _dec3 = dec,
139 _dec4 = dec2;
140```
141
142Next up, we define and evaluate the decorator expressions. The reason we
143do this _before_ defining the class is because we must interleave decorator
144expressions with computed property key expressions, since computed properties
145and decorators can run arbitrary code which can modify the runtime of subsequent
146decorators or computed property keys.
147
148```js
149let _Class;
150class Class {
151```
152
153This class is being decorated directly, which means that the decorator may
154replace the class itself. Class bindings are not mutable, so we need to create a
155new `let` variable for the decorated class.
156
157
158```js
159 static {
160 [
161 _init_a,
162 _call_b,
163 _init_c,
164 _get_c,
165 _set_c,
166 _Class,
167 _initClass,
168 _initProto,
169 _initStatic,
170 ] = _applyDecs(Class,
171 [
172 [_dec, 0, "a"],
173 [
174 _dec2,
175 7,
176 "b",
177 function () {
178 console.log('foo');
179 }
180 ],
181 [
182 [_dec4, _dec5],
183 1,
184 "c",
185 function () {
186 return this.#a;
187 },
188 function (value) {
189 this.#a = value;
190 }
191 ]
192 ],
193 [dec]
194 );
195
196 _initStatic(Class);
197 }
198```
199
200Next, we immediately define a `static` block which actually applies the
201decorators. This is important because we must apply the decorators _after_ the
202class prototype has been fully setup, but _before_ static fields are run, since
203static fields should only see the decorated version of the class.
204
205We apply the decorators to class elements and the class itself, and the
206application returns an array of values that are used to populate all of the
207local variables we defined earlier. The array's order is fully deterministic, so
208we can assign the values based on an index we can calculate ahead of time.
209
210Another important thing to note here is that we're passing some functions here.
211These are for private methods and accessors, which cannot be replaced directly
212so we have to extract their code so it can be decorated. Because we define these
213within the static block, they can access any private identifiers which were
214defined within the class, so it's not an issue that we're extracting the method
215logic here.
216
217We'll come back to `applyDecs` in a bit to dig into what its format is exactly,
218but now let's dig into the new definitions of our class elements.
219
220```js
221 a = (_initInstance(this), _init_a(this, 123));
222```
223
224Alright, so previously this was a simple class field. Since it's the first field
225on the class, we've updated it to immediately call `initInstance` in its
226initializer. This calls any initializers added with `addInitializer` for all of
227the per-class values (methods and accessors), which should all be setup on the
228instance before class fields are assigned. Then, it calls `_init_a` to get the
229initial value of the field, which allows initializers returned from the
230decorator to intercept and decorate it. It's important that the initial value
231is used/defined _within_ the class body, because initializers can now refer to
232private class fields, e.g. `a = this.#b` is a valid field initializer and would
233become `a = _init_a(this, this.#b)`, which would also be valid. We cannot
234extract initializer code, or any other code, from the class body because of
235this.
236
237Overall, this decoration is pretty straightforward other than the fact that we
238have to reference `_init_a` externally.
239
240```js
241 static #b(...args) {
242 _call_b(this, args);
243 }
244```
245
246Next up, we have a private static class method `#b`. This one is a bit more
247complex, as our definition has been broken out into 2 parts:
248
2491. `static #b`: This is the method itself, which being a private method we
250 cannot overwrite with `defineProperty`. We also can't convert it into a
251 private field because that would change its semantics (would make it
252 writable). So, we instead have it proxy to the locally scoped `_call_b`
253 variable, which will be populated with the fully decorated method.
2542. The definition of the method, kept in `_call_b`. As we mentioned above, the
255 original method's code is moved during the decoration process, and the wrapped
256 version is populated in `_call_b` and called whenever the private method is
257 called.
258
259```js
260 [_computedKey]() {}
261```
262
263Next is the undecorated method with a computed key. This uses the previously
264calculated and stored computed key.
265
266```js
267 #a = _init_c(this, 123);
268 get #c() {
269 return _get_c(this);
270 }
271 set #c(v) {
272 _set_c(this, v);
273 }
274```
275
276Next up, we have the output for `accessor #c`. This is the most complicated
277case, since we have to transpile the decorators, the `accessor` keyword, and
278target a private field. Breaking it down piece by piece:
279
280```js
281 #a = _init_c(this, 123);
282```
283
284`accessor #c` desugars to a getter and setter which are backed by a new private
285field, `#a`. Like before, the name of this field doesn't really matter, we'll
286just generate a short, unique name. We call the decorated initializer for `#c`
287and return that value to assign to the field.
288
289```js
290 get #c() {
291 return _get_c(this);
292 }
293 set #c(v) {
294 _set_c(this, v);
295 }
296```
297
298Next, we have the getter and setter for `#c` itself. These methods defer to
299the `_get_c` and `_set_c` local variables, which will be the decorated versions
300of the two getter functions that we passed for decoration in the static block
301above. Those two functions are essentially just accessors for the private `#a`
302field, but the decorator may add additional logic to them.
303
304```js
305 static {
306 _initClass(_Class);
307 }
308```
309
310Finally, we call `_initClass` in another static block, running any class and
311static method initializers on the final class. This is done in a static block
312for convenience with class expressions, but it could run immediately after the
313class is defined.
314
315Ok, so now that we understand the general output, let's go back to `applyDecs`:
316
317```js
318[
319 _init_a,
320 _call_b,
321 _init_c,
322 _get_c,
323 _set_c,
324 _Class,
325 _initClass,
326 _initProto,
327 _initStatic,
328] = _applyDecs(Class,
329 [
330 [_dec, 0, "a"],
331 [
332 _dec2,
333 7,
334 "b",
335 function () {
336 console.log('foo');
337 }
338 ],
339 [
340 [_dec4, _dec5],
341 1,
342 "c",
343 function () {
344 return this.#a;
345 },
346 function (value) {
347 this.#a = value;
348 }
349 ]
350 ],
351 [dec]
352);
353```
354
355`applyDecs` takes all of the decorators for the class and applies them. It
356receives the following arguments:
357
3581. The class itself
3592. Decorators to apply to class elements
3603. Decorators to apply to the class itself
361
362The format of the data is designed to be as minimal as possible. Here's an
363annotated version of the member descriptors:
364
365```js
366[
367 // List of decorators to apply to the field. Array if multiple decorators,
368 // otherwise just the single decorator itself.
369 dec,
370
371 // The type of the decorator, represented as an enum. Static-ness is also
372 // encoded by adding 5 to the values
373 // 0 === FIELD
374 // 1 === ACCESSOR
375 // 2 === METHOD
376 // 3 === GETTER
377 // 4 === SETTER
378 // 5 === FIELD + STATIC
379 // 6 === ACCESSOR + STATIC
380 // 7 === METHOD + STATIC
381 // 8 === GETTER + STATIC
382 // 9 === SETTER + STATIC
383 1,
384
385 // The name of the member
386 'y',
387
388 // Optional fourth and fifth values, these are functions passed for private
389 // decorators
390 function() {}
391],
392```
393
394Static and prototype decorators are all described like this. For class
395decorators, it's just the list of decorators since no other context
396is necessary.