ITADN

Proposal 2, for The Decorator Ordering Problem

#574Opentrusktr 创建于 2025-12-31
T
trusktrcommented
# The problem in general: See the following issues (most recent first) where "Proposal 1" is mentioned in comments such as [this one](https://github.com/tc39/proposal-decorators/issues/571#issuecomment-3652480940). This is a separate issue to outline a new second proposal in a clearer way. - https://github.com/tc39/proposal-decorators/issues/571 - https://github.com/tc39/proposal-decorators/issues/566 - https://github.com/tc39/proposal-decorators/issues/565 - https://github.com/tc39/proposal-decorators/issues/569 - https://github.com/tc39/proposal-decorators/issues/567 - https://github.com/tc39/proposal-decorators/issues/548 - https://github.com/tc39/proposal-decorators/issues/524 - https://github.com/tc39/proposal-decorators/issues/465 - https://github.com/tc39/proposal-decorators/issues/513 This problem has a colorful history. These issues all indicate that the current ordering of decorator applications and initializers are inflexible and limited, allowing only a certain set of use cases to be implemented in a simple way. This proposal aims to alleviate a large portion of that pain point, giving control to decorator authors, so that essentially all use cases can be implement in a very simple way. The sample code in this comment illustrates that certain types of non-decorator code today, cannot easily migrated to decorators in the future: - https://github.com/tc39/proposal-decorators/issues/565#issuecomment-3700641941 # A new solution: We could keep the existing API entirely as-is, but add a new API that allows decorator authors to add initializers that are executed in source definition order. The API could be something like this: 1. a new `context.addDefinitionOrderedInitializer(fn)` 2. or via a new option: `context.addInitializer(fn, {definitionOrder: true})` I'm thinking the second option looks better, and has room for additional ordering options needed to solve issues such as - https://github.com/tc39/proposal-decorators/issues/521 - https://github.com/tc39/proposal-decorators/issues/465 This new solution is nice because: - existing API continues to work as-is, for whoever is fine with it. - a new format like `context.addInitializer(fn, {definitionOrder: true})` (and `context.addInitializer(fn, {static: true})`, `context.addInitializer(fn, {postConstruct: true})`, etc, for other use cases) adds new timings for initializers for use cases that are currently either - much too complex to solve, - or have potentially undesirable outcomes such as converting synchronous construction logic into deferred microtask logic that causes a deadzone in consumer code in the current task in which construction happens. ## How it works The concept is simple: For instance initializers, any that are registered with `definitionOrder: true` simply run in definition (i.e. lexical, i.e. source-based) order. In this comment, - https://github.com/tc39/proposal-decorators/issues/565#issuecomment-3609368335 I've demonstrated an example that shows the ordering of both decorator applications, decorator non-extra initializers, and (current) decorator extra initializers. The ordering is inconcsistent between *all* groups, which is even more confusing than when looking at any one group on its own. This `Proposal 2` is a fix only for extra initializers, allowing extra initializers to be registered to run in definition order, and does not solve other ordering issues. With this solution in place, if the example from the aforementioned example has these lines, ```js context.addInitializer(function() { console.log('decorator extra initializer for:', context.name, context.kind) }) ``` changed to this, ```js context.addInitializer(function() { console.log('decorator extra initializer for:', context.name, context.kind) }, {definitionOrder: true}) ``` or similar (f.e. using `lexicalOrder: true` or `sourceOrder: true`), <details><summary>new code reproduced here for convenience:</summary> ```js function dec(value: any, context: any): any { console.log('decorator for:', context.name, context.kind) context.addInitializer(function() { console.log('decorator extra initializer for:', context.name, context.kind) }, {definitionOrder: true}) // ^------------------------ THIS NEW OPTIONS PARAMETER ADDED TO THE API if (context.kind === 'field') return function() { console.log('decorator storage initializer for:', context.name, context.kind) } if (context.kind === 'accessor') return { init() { console.log('decorator storage initializer for:', context.name, context.kind) } } } class My { @dec zero = 0 @dec accessor one = 1 @dec get two() {return 2} @dec set two(v) {} @dec three() {} @dec four = 4 @dec set five(v) {} @dec get five() {return 5} @dec accessor six = 6 @dec seven() {} } console.log('-----------------------') new My() ``` </details> then the output will change to the following: ``` decorator for: one accessor decorator for: two getter decorator for: two setter decorator for: three method decorator for: five setter decorator for: five getter decorator for: six accessor decorator for: seven method decorator for: zero field decorator for: four field ----------------------- decorator storage initializer for: zero field decorator extra initializer for: zero field decorator storage initializer for: one accessor decorator extra initializer for: one accessor decorator extra initializer for: two getter decorator extra initializer for: two setter decorator extra initializer for: three method decorator storage initializer for: four field decorator extra initializer for: four field decorator extra initializer for: five setter decorator extra initializer for: five getter decorator storage initializer for: six accessor decorator extra initializer for: six accessor decorator extra initializer for: seven method ``` This is easy to implement with Babel in TypeScript. They will simply - hook them after the non-definition-ordered, or non-extra initializer of a previous field, - or after any non-definition-ordered getter/setter/method extra initializer if no fields exist - or at the top of constructor (after super() if any) if the previous two don't exist. This is the technique that they already use in other cases, such as hooking non-definition-ordered extra initializers after of field initializers. --- For additional context, if the example code had both non-defition-ordered and definition-ordered extra initializers like this, ```js context.addInitializer(function() { console.log('decorator non-definition-ordered extra initializer for:', context.name, context.kind) }) context.addInitializer(function() { console.log('decorator definition-ordered extra initializer for:', context.name, context.kind) }, {definitionOrder: true}) ``` then the output would be: ``` decorator for: one accessor decorator for: two getter decorator for: two setter decorator for: three method decorator for: five setter decorator for: five getter decorator for: six accessor decorator for: seven method decorator for: zero field decorator for: four field ----------------------- decorator non-definition-ordered extra initializer for: two getter decorator non-definition-ordered extra initializer for: two setter decorator non-definition-ordered extra initializer for: three method decorator non-definition-ordered extra initializer for: five setter decorator non-definition-ordered extra initializer for: five getter decorator non-definition-ordered extra initializer for: seven method decorator storage initializer for: zero field decorator non-definition-ordered extra initializer for: zero field decorator definition-ordered extra initializer for: zero field decorator storage initializer for: one accessor decorator non-definition-ordered extra initializer for: one accessor decorator definition-ordered extra initializer for: one accessor decorator definition-ordered extra initializer for: two getter decorator definition-ordered extra initializer for: two setter decorator definition-ordered extra initializer for: three method decorator storage initializer for: four field decorator non-definition-ordered extra initializer for: four field decorator definition-ordered extra initializer for: four field decorator definition-ordered extra initializer for: five setter decorator definition-ordered extra initializer for: five getter decorator storage initializer for: six accessor decorator non-definition-ordered extra initializer for: six accessor decorator definition-ordered extra initializer for: six accessor decorator definition-ordered extra initializer for: seven method ``` In particular note that if we filter to see only the outputs of definition-ordered extra initializers, we get: ``` decorator definition-ordered extra initializer for: zero field decorator definition-ordered extra initializer for: one accessor decorator definition-ordered extra initializer for: two getter decorator definition-ordered extra initializer for: two setter decorator definition-ordered extra initializer for: three method decorator definition-ordered extra initializer for: four field decorator definition-ordered extra initializer for: five setter decorator definition-ordered extra initializer for: five getter decorator definition-ordered extra initializer for: six accessor decorator definition-ordered extra initializer for: seven method ``` where each definition-ordered extra initializer will always execute *after* both - the non-extra initializer - and all non-definition-ordered extra initializers for the same class member. # Aside: other ordering options This could allow other options to be considered: - `context.addInitializer(fn, {static: true})` - on an instance member decoration, this causes an extra initializer to run after the class definition, along with static members and decorators, in non-definition order. All static-bound instance-member extra initializers run after all static-member extra initializers. - this would avoid having to also use a class decorator for certain cases, which is error prone. - `context.addInitializer(fn, {static: true, definitionOrder: true})` - the same as the previous, but in definition order, instance-member extra initializers run interleaved with static-member extra initializers, use definition order for control. - `context.addInitializer(fn, {postConstruct: true})` - causes an extra initializer to run immediately after the constructor call stack, in non-definition order - the moment a subclass does not return the same `this`, post construction extra initializers are called and control of subequent values that are not `this` is lost - `context.addInitializer(fn, {postConstruct: true, definitionOrder: true})` - the same as the previous, but in definition order. - `context.addInitializer(fn, {postConstruct: true})` Anything that might not make sense, such as `context.addInitializer(fn, {postConstruct: true, static: true})`, could throw. # Aside: decorator *application* still has undesirable ordering (That's the content above the line `---` in the example output from above.) This proposal doesn't solve that directly, but at least we'd have a workaround: running logic in definition-ordered extra initializers of the _first_ instance of a class would do the trick for the vast majority of any use cases that need definition ordering. Where the workaround wouldn't work is cases that need to know member ordering before any instance is ever created, f.e. by reading info from `metadata`. The idea of `context.addInitializer(fn, {static: true, definitionOrder: true})` is a nicer solution to that problem (when the order is needed before the first instance is ever made), without changing the default decorator application order. # My hope is that a solution can be found, for the sake of simplicity for various end use cases. One such use case is the simple _forward migration_ of any existing non-decorator libraries to decorators.
1 条评论