Closed lukeburns closed 6 years ago
Hi!
This is normal, but you probably want to do something different.
By default reactions are executed synchronously on observable mutations. This usually results in reactions running a lot of times for complex mutations.
There is a lot going on when you do list.push('World!')
. list.length
, list[1]
and a hidden key for object enumeration changes. console.log(list)
uses list.length
and the hidden enumeration key to do its work so it updates twice (on each of the two key mutations). This will just get worse with more complex functions. I don't know your use case, but I am pretty sure that you don't want to run reactions synchronously on observable mutations.
You can pass a scheduler to observe. In this case the triggered reaction is not executed synchronously on observable mutations, but passed to the scheduler function instead. Take a look at this example:
import { observe, observable } from '@nx-js/observer-util'
import { Queue, priorities } from '@nx-js/queue-util'
const list = observable(['Hello'])
const scheduler = new Queue(priorities.LOW)
observe(() => console.log(list), { scheduler })
// this passes the reaction to the scheduler
// the scheduler logs ['Hello', 'World!'] once when the browser/Node has some free time
list.push('World!')
Typically a scheduler does two things:
priority
.You can use custom schedulers, but I advise you to use the @nx-js/queue-util. You can learn more about schedulers in this docs section, be sure the check all three examples.
I hope this helped 🙂
Edit: By using a bunch of schedulers with different priorities, you can implement something similar to React Fiber
Edit2: A debugger option is under development for observe
, which gives you an exact reason for every reaction trigger. It shows you which observable property mutation scheduled the reaction and where does the reaction uses the specific observable property.
@lukeburns can I close this?
Yep! Thanks for the helpful explanation, and the update on the debug option under development.
I tweaked this behavior a bit in v4.1.0. From now on any atomic operation is guaranteed to schedule a reaction maximum once. By atomic operation I mean a JS operation, which is implemented in none interceptable native code (like property get/set).
Also I added a new experimental debugger. The easiest way of trying it out is this:
let dummy
const person = observable({ name: 'Bob' })
observe(() => dummy = person.name, { debugger: console.log })
person.name = 'Rick'
This logs the context of every single operation that is related to the observed reaction. It will log that person.name is used with a get
operation in the reaction and that person.name
changed value later. From this stream of data you can deduce why The Observer Util decided to schedule the reaction. The debugger API is designed to be raw. It pushes out a lot of metadata, which meant to be aggregated and filtered by 3rd party libs for larger projects.
Thanks for your work on this. This change certainly gives me the behavior I originally expected with the code above!
However, I'm receiving a Max call stack size exceeded
error with the dummy code you shared above.
Additionally,
const person = observable({ name: 'Bob' })
observe(() => console.log(person))
person.name = 'Rick'
only logs { name: 'Bob' }
, when I expect { name: 'Rick' }
to also be logged — as in version 4.0.
What platform are you on? Did you get this result in NodeJS or the browser? (For me it seems to be only buggy in NodeJS).
Interesting! I'm using node. Wonder why it would be platform specific. Different Proxy implementations?
I fixed the first (infinite loop issue) in the 4.1.1 release. Thanks for catching the bug 🙂
The second issue is specific to the NodeJS console.log
implementation. Normal for in loops
and object enumeration works, but apparently NodeJS console.log is implemented in native code. It is not getting person.name
under the hood (you can see this with the now working debugger).
I will see if this can or should be fixed. Is this a big issue for you? I imagine this would only be used for debugging purposes.
Edit: instead of getting each property key, Node gets a shady inspect
prop for console.log. I guess Object.prototype has a custom inspect
prop in Node which points to some native magic.
I'm confused about the behavior of the following code:
This logs
['Hello', 'World!']
twice. I expect it to log once. I get expected behavior from:Is this the correct behavior?