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

How does runtime subscription work? #248

Closed
daimeng opened this Issue May 13, 2016 · 4 comments

Comments

Projects
None yet
3 participants
@daimeng
Copy link

daimeng commented May 13, 2016

Hi guys. A little background. Currently working as part of a small group to completely overhaul a backbone frontend. I've chosen MobX recently as our primary state management candidate to use with React. We have a large team to expand this to later potentially, so I needed something easy to ramp up. While I find the overall concepts easy to follow, there are some parts that still seem like black magic to me.

I want to get around to reading through the source for this library later, but I'm completely bogged down. My biggest question is how runtime subscription works. Maybe I'm missing something obvious, but how does say autorun(() => console.log(observableVar)) know what variables were used inside its function argument?

@AriaFallah

This comment has been minimized.

Copy link

AriaFallah commented May 13, 2016

@daimeng

MobX tries to act as much like a proxy sitting in front of native JavaScript as it can without being able to use ES6 proxies. All of the values inside observables are converted to a corresponding class that tries to emulate the original value as much as it possibly can while making it observable. For example, MobX arrays aren't actually native JavaScript arrays. Internally observable arrays use a JS array, but the class has to redefine all the array prototype methods such that they reportObserved or reportChanged before actually calling the method on the internal collection. It's similar with every other observable type as well. Knowing this basic overview of how MobX is designed we can get into how autorun subscribes to changes.

autorun will register your function to run again whenever reportObserved() is called, and if it isn't called, that autorun won't run again, meaning that if you throw an observable inside console.log(observable) then autorun won't run again. You need to do it such that it's console.log(observable.someMethodThatCallsReportObserved).

To give a more concrete example, if you had:

const x = observable([])
autorun(() => console.log(x))

x.push(1)
x.push(2)
x.push(3)

the autorun would only run once. MobX would run console.log(x), realize that nothing inside that calls reportObserved, and will never run again.

So instead you think to yourself, how about I log x[0] because I know x[someValue] calls reportObserved():

const x = observable([])
autorun(() => console.log(x[0]))

x.push(1)
x.push(2)
x.push(3)

autorun will still only run once because that's only true if x[someValue] isn't undefined, but at the time of the autorun x[0] had no value.


So how do you get autorun to run 100% of the time? With arrays it's as simple as calling slice()

const x = observable([])
autorun(() => console.log(x.slice()))

x.push(1)
x.push(2)
x.push(3)

and

const x = observable([])
autorun(() => console.log(x.slice()[0]))

x.push(1)
x.push(2)
x.push(3)

for the respective problems.

Likewise with plain observable values to log on change it'd be

const x = observable(1)
autorun(() => console.log(x.get()))
x.set(1)
x.set(2)
x.set(3)

With observable objects it's tricky to log the whole thing on change, but it'd be like

const x = observable({ x: 1, y: 2 })
autorun(() => Object.keys(x).forEach((key) => (console.log(x[key])))) // x[key] is like calling .get() on the value
x.x = 2 // equivalent to x.set() for observable values
x.y = 3 // same as above ^

for observable objects note that the keys have to exist in advance otherwise they won't be observable.

With MobX maps to log the whole thing on change it'd be

const x = map({})
autorun(() => console.log(x.toJS()))
x.set('x', 1)
x.set('y', 2)

The gist of the idea here is that you need to find a reliable way to trigger reportObserved, the easiest of which is just by calling the respective defensive copying methods of each observable type .slice() for arrays .get() for plain values and .toJs() for maps.

@mweststrate correct me if I'm wrong on anything. It also does make me think computed(() => doStuff()).observe() should be recommended over autorun to make it easier to understand what's actually going on.

@mweststrate

This comment has been minimized.

Copy link
Member

mweststrate commented May 13, 2016

@daimeng in two lines: autorun and computed put the function the execute on a stack. Whenever an observable values is accessed, MobX associates that value with the function that was started by autorun / computed. Now if that value changes in the future, the associated autorun / `computeds will be retriggered. It is explained in greater depth here.

[offtopic]
@AriaFallah yes computed(expr).observe(effect) might make it a bit more clear. (autorun is actually computed(expr).observe(noop)). I'm thinking about introducing this as explicit concept in the api, but I'm not sure how to name it yet. As overload for autorun, or observe, or under a separate name (react(ion))? Note that this pattern might be confusing in another way, if expr just returns an object, but the individual members are only accessed in the effect, those members wouldn't be tracked. With autorun you cannot make thát mistake. So still doubting what is the best thing to recommend. Note that in MobX 2.2 autorun(() => someAction(expr)) would also have the same effect as computed(expr).observe(effect) if const someAction = action(data => effect). So there are many ways to express the same thing, just looking for what is the most clear :)

@daimeng

This comment has been minimized.

Copy link
Author

daimeng commented May 15, 2016

@AriaFallah Thanks for the explanations. I've been playing around with some of your examples in jsfiddle and how observable works with different kinds of values is becoming a lot more clear to me now. This is a really useful reference to keep.

@mweststrate Yes I read that article! It was a large part of what convinced me to try MobX. At the time I did understand in some capacity that the runtime subscription worked by a proxy-ish behavior. But while reading the article, I had assumed (stupidly) that autorun function was not run once initially. I thought somehow the values were being discovered in another way for autorun. Of course, this assumption made no sense and I found that out with further testing.

@mweststrate

This comment has been minimized.

Copy link
Member

mweststrate commented May 17, 2016

Closed, question is answered. (I think ;-))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment