Skip to content

Commit 0973aea

Browse files
authored
Fix decorators (mobxjs#313)
Improved decorators; faster, more consistent in babel and TS, more code reuse
1 parent 36df55c commit 0973aea

9 files changed

Lines changed: 746 additions & 243 deletions

File tree

src/api/computeddecorator.ts

Lines changed: 26 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,34 @@
11
import {ValueMode, getValueModeFromValue, asStructure} from "../types/modifiers";
22
import {IObservableValue} from "./observable";
3-
import {asObservableObject, setObservableObjectProperty} from "../types/observableobject";
4-
import {invariant, assertPropertyConfigurable} from "../utils/utils";
3+
import {asObservableObject, defineObservableProperty} from "../types/observableobject";
4+
import {invariant} from "../utils/utils";
5+
import {createClassPropertyDecorator} from "../utils/decorators";
56
import {ComputedValue} from "../core/computedvalue";
67

78
export interface IComputedValueOptions {
89
asStructure: boolean;
910
}
1011

12+
const computedDecorator = createClassPropertyDecorator(
13+
(target, name, _, decoratorArgs, originalDescriptor) => {
14+
const baseValue = originalDescriptor.get;
15+
invariant(typeof baseValue === "function", "@computed can only be used on getter functions, like: '@computed get myProps() { return ...; }'");
16+
17+
let compareStructural = false;
18+
if (decoratorArgs && decoratorArgs.length === 1 && decoratorArgs[0].asStructure === true)
19+
compareStructural = true;
20+
21+
const adm = asObservableObject(target, undefined, ValueMode.Recursive);
22+
defineObservableProperty(adm, name, compareStructural ? asStructure(baseValue) : baseValue, false);
23+
},
24+
function (name) {
25+
return this.$mobx.values[name].get();
26+
},
27+
throwingComputedValueSetter,
28+
false,
29+
true
30+
);
31+
1132
/**
1233
* Decorator for class properties: @computed get value() { return expr; }.
1334
* For legacy purposes also invokable as ES5 observable created: `computed(() => expr)`;
@@ -17,7 +38,8 @@ export function computed(opts: IComputedValueOptions): (target: Object, key: str
1738
export function computed(target: Object, key: string | symbol, baseDescriptor?: PropertyDescriptor): void;
1839
export function computed(targetOrExpr: any, keyOrScope?: any, baseDescriptor?: PropertyDescriptor, options?: IComputedValueOptions) {
1940
if (arguments.length < 3 && typeof targetOrExpr === "function")
20-
return computedExpr(targetOrExpr, keyOrScope)
41+
return computedExpr(targetOrExpr, keyOrScope);
42+
invariant(!baseDescriptor || !baseDescriptor.set, `@observable properties cannot have a setter: ${keyOrScope}`);
2143
return computedDecorator.apply(null, arguments);
2244
}
2345

@@ -26,43 +48,6 @@ function computedExpr<T>(expr: () => T, scope?: any) {
2648
return new ComputedValue(value, scope, mode === ValueMode.Structure, value.name);
2749
}
2850

29-
function computedDecorator(target: any, key?: any, baseDescriptor?: PropertyDescriptor, options?: IComputedValueOptions): any {
30-
// invoked as decorator factory with options
31-
if (arguments.length === 1) {
32-
const options = target;
33-
return (target, key, baseDescriptor) => computedDecorator.call(null, target, key, baseDescriptor, options);
34-
}
35-
invariant(baseDescriptor && baseDescriptor.hasOwnProperty("get"), "@computed can only be used on getter functions, like: '@computed get myProps() { return ...; }'");
36-
assertPropertyConfigurable(target, key);
37-
38-
const descriptor: PropertyDescriptor = {};
39-
const getter = baseDescriptor.get;
40-
41-
invariant(typeof target === "object", `The @observable decorator can only be used on objects`, key);
42-
invariant(typeof getter === "function", `@observable expects a getter function if used on a property.`, key);
43-
invariant(!baseDescriptor.set, `@observable properties cannot have a setter.`, key);
44-
invariant(getter.length === 0, `@observable getter functions should not take arguments.`, key);
45-
46-
descriptor.configurable = true;
47-
descriptor.enumerable = false;
48-
descriptor.get = function() {
49-
setObservableObjectProperty(
50-
asObservableObject(this, undefined, ValueMode.Recursive),
51-
key,
52-
options && options.asStructure === true ? asStructure(getter) : getter
53-
);
54-
return this[key];
55-
};
56-
57-
// by default, assignments to properties without setter are ignored. Let's fail fast instead.
58-
descriptor.set = throwingComputedValueSetter;
59-
if (!baseDescriptor) {
60-
Object.defineProperty(target, key, descriptor); // For typescript
61-
} else {
62-
return descriptor;
63-
}
64-
}
65-
6651
export function throwingComputedValueSetter() {
6752
throw new Error(`[ComputedValue] It is not allowed to assign new values to computed properties.`);
68-
}
53+
}

src/api/extendobservable.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {ValueMode} from "../types/modifiers";
22
import {ObservableMap} from "../types/observablemap";
3-
import {asObservableObject, setObservableObjectProperty} from "../types/observableobject";
3+
import {asObservableObject, setObservableObjectInstanceProperty} from "../types/observableobject";
44
import {invariant, isPropertyConfigurable} from "../utils/utils";
55

66
/**
@@ -25,7 +25,7 @@ export function extendObservableHelper(target, properties, mode: ValueMode, name
2525
for (let key in properties) if (properties.hasOwnProperty(key)) {
2626
if (target === properties && !isPropertyConfigurable(target, key))
2727
continue; // see #111, skip non-configurable or non-writable props for `observable(object)`.
28-
setObservableObjectProperty(adm, key, properties[key]);
28+
setObservableObjectInstanceProperty(adm, key, properties[key]);
2929
}
3030
return target;
3131
}

src/api/observabledecorator.ts

Lines changed: 25 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,30 @@
11
import {ValueMode, asReference} from "../types/modifiers";
22
import {allowStateChanges} from "../core/action";
3-
import {computed} from "../api/computeddecorator";
4-
import {asObservableObject, setObservableObjectProperty} from "../types/observableobject";
5-
import {invariant, assertPropertyConfigurable, deprecated} from "../utils/utils";
6-
import {checkIfStateModificationsAreAllowed} from "../core/derivation";
3+
import {asObservableObject, defineObservableProperty, setPropertyValue} from "../types/observableobject";
4+
import {invariant, assertPropertyConfigurable} from "../utils/utils";
5+
import {createClassPropertyDecorator} from "../utils/decorators";
6+
7+
const decoratorImpl = createClassPropertyDecorator(
8+
(target, name, baseValue) => {
9+
allowStateChanges(true, () => {
10+
if (typeof baseValue === "function")
11+
baseValue = asReference(baseValue);
12+
const adm = asObservableObject(target, undefined, ValueMode.Recursive);
13+
defineObservableProperty(adm, name, baseValue, false);
14+
});
15+
},
16+
function (name) {
17+
return this.$mobx.values[name].get();
18+
},
19+
function (name, value) {
20+
setPropertyValue(this, name, value);
21+
},
22+
true,
23+
false
24+
);
725

826
/**
9-
* ES6 / Typescript decorator which can to make class properties and getter functions reactive.
27+
* ESNext / Typescript decorator which can to make class properties and getter functions reactive.
1028
* Use this annotation to wrap properties of an object in an observable, for example:
1129
* class OrderLine {
1230
* @observable amount = 3;
@@ -19,40 +37,6 @@ import {checkIfStateModificationsAreAllowed} from "../core/derivation";
1937
export function observableDecorator(target: Object, key: string, baseDescriptor: PropertyDescriptor) {
2038
invariant(arguments.length >= 2 && arguments.length <= 3, "Illegal decorator config", key);
2139
assertPropertyConfigurable(target, key);
22-
23-
// - In typescript, observable annotations are invoked on the prototype, not on actual instances,
24-
// so upon invocation, determine the 'this' instance, and define a property on the
25-
// instance as well (that hides the propotype property)
26-
// - In babel, the initial value is passed as the closure baseDiscriptor.initializer'
27-
28-
if (baseDescriptor && baseDescriptor.hasOwnProperty("get")) {
29-
deprecated("Using @observable on computed values is deprecated. Use @computed instead.");
30-
return computed.apply(null, arguments);
31-
}
32-
const descriptor: PropertyDescriptor = {};
33-
34-
invariant(typeof target === "object", `The @observable decorator can only be used on objects`, key);
35-
descriptor.configurable = true;
36-
descriptor.enumerable = true;
37-
descriptor.get = function() {
38-
let baseValue = undefined;
39-
if (baseDescriptor && (<any>baseDescriptor).initializer) { // For babel
40-
baseValue = (<any>baseDescriptor).initializer();
41-
if (typeof baseValue === "function")
42-
baseValue = asReference(baseValue);
43-
}
44-
// the getter might create a reactive property lazily, so this might even happen during a view.
45-
allowStateChanges(true, () => {
46-
setObservableObjectProperty(asObservableObject(this, undefined, ValueMode.Recursive), key, baseValue);
47-
});
48-
return this[key];
49-
};
50-
descriptor.set = function(value) {
51-
setObservableObjectProperty(asObservableObject(this, undefined, ValueMode.Recursive), key, typeof value === "function" ? asReference(value) : value);
52-
};
53-
if (!baseDescriptor) {
54-
Object.defineProperty(target, key, descriptor); // For typescript
55-
} else {
56-
return descriptor;
57-
}
40+
invariant(!baseDescriptor || !baseDescriptor.get, "@observable can not be used on getters, use @computed instead");
41+
return decoratorImpl.apply(null, arguments);
5842
}

src/core/action.ts

Lines changed: 34 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -4,90 +4,47 @@ import {untracked} from "../core/derivation";
44
import {isSpyEnabled, spyReportStart, spyReportEnd} from "../core/spy";
55
import {ComputedValue} from "../core/computedvalue";
66
import {globalState} from "../core/globalstate";
7+
import {createClassPropertyDecorator} from "../utils/decorators";
8+
9+
const actionDecorator = createClassPropertyDecorator(
10+
function (target, key, value, args, originalDescriptor) {
11+
const actionName = (args && args.length === 1) ? args[0] : (value.name || key || "<unnamed action>");
12+
const wrappedAction = action(actionName, value);
13+
if (originalDescriptor && originalDescriptor.value && target.constructor && target.constructor.prototype) {
14+
// shared method, replace this very property on the prototype with the right value
15+
Object.defineProperty(target.constructor.prototype, key, {
16+
configurable: true, enumerable: false, writable: false,
17+
value: wrappedAction
18+
});
19+
} else {
20+
// bound instance methods
21+
Object.defineProperty(target, key, {
22+
configurable: true, enumerable: false, writable: false,
23+
value: wrappedAction
24+
});
25+
}
26+
},
27+
function (key) {
28+
return this[key];
29+
},
30+
function () {
31+
invariant(false, "It is not allowed to assign new values to @action fields");
32+
},
33+
false,
34+
true
35+
);
736

837
export function action<T extends Function>(fn: T): T;
938
export function action<T extends Function>(name: string, fn: T): T;
1039
export function action(customName: string): (target: Object, key: string, baseDescriptor?: PropertyDescriptor) => void;
1140
export function action(target: Object, propertyKey: string, descriptor?: PropertyDescriptor): void;
1241
export function action(arg1, arg2?, arg3?, arg4?): any {
13-
switch (arguments.length) {
14-
case 1:
15-
// action(someFunction)
16-
if (typeof arg1 === "function")
17-
return actionImplementation(arg1.name || "<unnamed action>", arg1);
18-
// @action("custom name") someFunction () {}
19-
// @action("custom name") someFunction = () => {}
20-
else
21-
return (target, key, descriptor) => actionDecorator(arg1, target, key, descriptor);
22-
case 2:
23-
// action("custom name", someFunction)
24-
if (typeof arg2 === "function")
25-
return actionImplementation(arg1, arg2);
26-
else
27-
return actionDecorator(arg2, arg1, arg2, undefined); // See #269
28-
case 3:
29-
// @action someFunction () {}
30-
// @action someFunction = () => {}
31-
return actionDecorator(arg2, arg1, arg2, arg3);
32-
default:
33-
invariant(false, "Invalid arguments for (@)action, please provide a function, name and function or use it as decorator on a class instance method");
34-
}
35-
}
36-
37-
function actionDecorator(name: string, target: any, key: string, descriptor: PropertyDescriptor) {
38-
if (descriptor === undefined) {
39-
// typescript: @action f = () => { }
40-
typescriptActionValueDecorator(name, target, key);
41-
return;
42-
}
43-
if (descriptor.value === undefined && typeof (descriptor as any).initializer === "function") {
44-
// typescript: @action f = () => { }
45-
return babelActionValueDecorator(name, target, key, descriptor);
46-
}
47-
const base = descriptor.value;
48-
descriptor.value = actionImplementation(name, base);
49-
}
50-
51-
/**
52-
* Decorators the following pattern @action method = () => {} by desugaring it to method = action(() => {})
53-
*/
54-
function babelActionValueDecorator(name: string, target, prop, descriptor): PropertyDescriptor {
55-
return {
56-
configurable: true,
57-
enumerable: false,
58-
get: function() {
59-
const v = descriptor.initializer.call(this);
60-
invariant(typeof v === "function", `Babel @action decorator expects the field '${prop} to be initialized with a function`);
61-
const implementation = action(name, v);
62-
addBoundAction(this, prop, implementation);
63-
return implementation;
64-
},
65-
set: function() {
66-
invariant(false, `Babel @action decorator: field '${prop}' not initialized`);
67-
}
68-
};
69-
}
70-
71-
function typescriptActionValueDecorator(name: string, target, prop) {
72-
Object.defineProperty(target, prop, {
73-
configurable: true,
74-
enumerable: false,
75-
get: function() {
76-
invariant(false, `TypeScript @action decorator: field '${prop}' not initialized`);
77-
},
78-
set: function(v) {
79-
invariant(typeof v === "function", `TypeScript @action decorator expects the field '${prop} to be initialized with a function`);
80-
addBoundAction(this, prop, action(name, v));
81-
}
82-
});
83-
}
42+
if (arguments.length === 1 && typeof arg1 === "function")
43+
return actionImplementation(arg1.name || "<unnamed action>", arg1);
44+
if (arguments.length === 2 && typeof arg2 === "function")
45+
return actionImplementation(arg1, arg2);
8446

85-
function addBoundAction(target, prop, implementation) {
86-
Object.defineProperty(target, prop, {
87-
enumerable: false,
88-
writable: false,
89-
value: implementation
90-
});
47+
return actionDecorator.apply(null, arguments);
9148
}
9249

9350
export function actionImplementation(actionName: string, fn: Function): Function {

0 commit comments

Comments
 (0)