Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved observable api #1365

Merged
merged 46 commits into from
Mar 3, 2018
Merged

Improved observable api #1365

merged 46 commits into from
Mar 3, 2018

Conversation

mweststrate
Copy link
Member

Improved observable api

@mweststrate mweststrate mentioned this pull request Mar 1, 2018
14 tasks
fail(
`'extendObservable' can only be used to introduce new properties. Use 'decorate' to update existing properties. The property '${key}' already exists on '${target}'`
)
if (isComputed(value))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It shouldn't throw, but simply assign boxed computed:

const o = {}
mobx.extendObservable(o, {
  boxedComputed: mobx.computed(() => "a"), 
});
console.log(o.boxedComputed.get());

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would create an observable references to a computed object, it is barely imaginable that that is what the user would intend, so throwing and telling how to properly do it is better imho.

If he really needs a boxed computed, he can just do o.boxedComputed = mobx.computed(). Or in the weird case hey needs an observable ref to it, extending it with null first, and then assigning it would work fine.

@urugator
Copy link
Collaborator

urugator commented Mar 1, 2018

If you try to re-decorate prop (via decorate/extendObservable) it throws:
@observable can not be used on getters, use @computed instead
Because it tries to apply decorator on mobx generated getter...

defineObservablePropertyFromDescriptor(adm, key, descriptor!, defaultEnhancer)
const decorators: any = {}
for (let key in properties) {
// TODO: using decorators is a bit inefficient, short circuit those
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you mean to directly define observable on target based on decorator type, then it can't be used on prototypes ...?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that we can't work with prototypes in the same way as with objects directly. It is kinda stupid, but the iterability will be wrong, if you define a property with getter / setter on the prototype, the properties won't be interated by most tools that iterate objects, because they are not own properties of the instances. So JSON.stringify, console.log, Object.keys all will behave weird.

Hence the decorator version is much more complex then the non-decorator implementation. Basically; it creates a property on the prototype / object, that, upon first read, will create a new property definition on this.

In the implementation in this branch I only used the decorator based implementation to test the new api, but it is effectively twice as slow. This is also the reason decorate and extendObservable will both remain as api; extendObservable (and observable.object) can short circuit that process because they should always be used on instances.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In pseudo code, it is something like this:


function decorator(target, prop) {
    Object.defineProperty(target, prop, {
        enumerable: true, 
        configurable: true,
        get() {
            return this._x
        },
        set(v) {
            // on first write, redeclare the prop (also do on first read btw)
            Object.defineProperty(this, prop, {
                enumerable: true,
                get() {
                    return 3
                },
                set(v) {
                    this._x = v * 2
                }
            })
            this._x = v * 2
        }
    })
}

class X {
    @decorator y = 3
}

const x = new X()

console.dir(JSON.stringify(x))

I have some ideas on how that can be improved though, but not sure yet if it will work out :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

N.B, note that the above still has weird behavior;

const x = new X()
Object.getOwnPropertyNames(x) // empty set!

The reason is; before the property is read or written once, it does't exist on the instance yet. This is luckily a problem that will be fixed in the new decorators proposal.

Copy link
Collaborator

@urugator urugator Mar 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea I think I understand that, but basically it's just a peformance problem ... if target is not prototype, then "enumberable" getters are defined directly on the object, therefore are "own" and enumerable... and if target is prototype then props are not "owned" by instance until first get ... therefore it doesn't introduce any new problems ...

So, if I understand correctly, after optimization, the decorators passed to extendObservable won't be actually called... therefore the depedency on decorators is technically redundat and for example strings/symbols could be used instead...?:

extendObservable(o, values, {
  a: "ref",
  b: "deep",
})

@mweststrate
Copy link
Member Author

If you try to re-decorate prop (via decorate/extendObservable) it throws:
@observable can not be used on getters, use @computed instead
Because it tries to apply decorator on mobx generated getter...

Do you have an example test case?

@urugator
Copy link
Collaborator

urugator commented Mar 1, 2018

const o = {};
mobx.extendObservable(o, {
  a: 5,  
});
mobx.extendObservable(o, {}, {
  a: mobx.observable.ref // a is getter at this point
});

I tried it in that codesandbox you provided with updated dep to alpha2

@mweststrate
Copy link
Member Author

@urugator that is not supposed to work; extendObservable is only allowed to declare new properties (previously it was allowed, but it makes for way to many weird edge cases, including ones where you could easily leak observables)

@urugator
Copy link
Collaborator

urugator commented Mar 1, 2018

Well ok, but can we do something about the error...?

@mweststrate
Copy link
Member Author

yes, good one

@mweststrate
Copy link
Member Author

mweststrate commented Mar 1, 2018 via email

@coveralls
Copy link

Coverage Status

Coverage decreased (-1.3%) to 92.84% when pulling 390a852 on improved-observable-api into 3bde5bc on mobx4.

@mweststrate mweststrate merged commit 390a852 into mobx4 Mar 3, 2018
@mweststrate mweststrate deleted the improved-observable-api branch March 7, 2018 13:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants