observer-util: [Optimization] Don't trigger observers if prev value and new value are same

For this snippet -

const observer  = require("@risingstack/nx-observe");

let context = observer.observable({"name" : "a"});

const signal = observer.observe(()=>console.log(context.name));

let name = context.name;

context.name = "a";

The observer is triggered twice. Wouldn’t it help to not trigger observers if the value hasn’t changed?

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Comments: 45 (23 by maintainers)

Most upvoted comments

nx-observe 3.0.0 is published. It comes with all of the improvements mentioned above.

The examples you mentioned above has the following result with v3.0.0.

1, code:

const observer  = require("@risingstack/nx-observe");

let context = observer.observable({"person" : {name:"a"}});

const signal = observer.observe(()=>console.log(context.person));

setTimeout(()=>context.person.name = "a", 100);
setTimeout(()=>context.person.name = "a", 500);
setTimeout(()=>context.person.name = "a", 1000);
setTimeout(()=>context.person.name = "b", 2000);
setTimeout(()=>context.person.name = "c", 3000);

output:

{ name: 'a' }
{ name: 'b' }
{ name: 'c' }

2, code:

const observer  = require("@risingstack/nx-observe");

let trails = [];

let context = observer.observable({"candidates" : {"first" : {"id" : "first", "name":"Amy"}}});

const signal = observer.observe(()=>trails.push(JSON.stringify(context.candidates)));

setTimeout(()=>context.candidates.second = {"id" : "second", "name":"Tracy"}, 100);
setTimeout(()=>context.candidates.third = {"id" : "third", "name":"Joe"}, 500);

setTimeout(()=>console.log(trails), 2000);

output:

[ '{"first":{"id":"first","name":"Amy"}}',
  '{"first":{"id":"first","name":"Amy"},"second":{"id":"second","name":"Tracy"}}',
  '{"first":{"id":"first","name":"Amy"},"second":{"id":"second","name":"Tracy"},"third":{"id":"third","name":"Joe"}}' ]

@ippa So far the integration has been good 😃, @solkimicreb has been very helpful and has helped us a lot by fixing issues that came along the way. The implementation is very much like with mobx and design considerations are also similar. The project is not open source yet and I have been working on other challenges with it. But I will try to share a simple example soon on the discussion which @solkimicreb spun from this.

Tested and working great, thanks!

Fixed and merged.

From version 2.0.5 set operations without a value change will not trigger observers. This version also fixes built-in object support (Set, Map, WeakSet, WeakMap, Date, String, etc). Tests and readMe is also updated.

First synchronous run will still happen and the config function didn’t make it (caused more trouble then good).

I am closing the issue, we can discuss async mode in another issue later maybe.

I tested the PR today. It’s only reporting a single change in my test code… image

Got it, and thanks for the in-depth explanation. There are easy workarounds of the multiple renders for me, so this won’t be a blocker 😃. As this only impacts value types, a simple shallow comparison in my React component will take care of multiple re-renders.

Should I close this issue?

Okay probably I am having a really long day. But after trying out with more combinations, in async mode, the observers are invoked always, even when the new value is exactly the same as the old one. Here’s the complete snippet just to ensure I haven’t screwed it again -

const observer  = require("@risingstack/nx-observe");

let context = observer.observable({"name" : "a"});

const signal = observer.observe(()=>console.log(context.name));

setTimeout(()=>context.name = "a", 100);
setTimeout(()=>context.name = "a", 500);
setTimeout(()=>context.name = "a", 1000);
setTimeout(()=>context.name = "b", 2000);
setTimeout(()=>context.name = "c", 3000);

The output is -

a
a <- not needed
a <- not needed
a <- not needed
b
c

You just found some deep stuff, that needs some explanation (:

Observed functions always run once synchronously after you pass them to observe(). This is needed to expose a predictable synchronous behavior (similarly to mobx.)

After that observer functions are never triggered synchronously on changes, but collected in a Set instead. This set removes the duplicates over time and after the stack empties (the current batch of synchronous code finishes) all the triggered observed functions are executed (without duplicates). This ensures that no observed function runs more than once per stack. The following would only run once and it would print ‘c’ to the console.

let context = observer.observable({"name" : "a"});

context.name = "a";
context.name = "b";
context.name = "c";

This is preferred by most observable libraries (not by mobx though, but it has ‘transactions’ and ‘actions’ instead).

Your example

In your example you registered a new observed function, which caused it to run synchronously. Then you triggered it with an observable change, that made it run after the current stack emptied. It is the only situation when an observed function can run more than once per stack. And even in this situation it runs max twice.

About not triggering on same value

This would be nice but it causes some problems with built in objects (like Array, String, etc). Built-ins have internal implementation which should be intercepted by Proxies, but it is still buggy in some cases (these are being fixed over time). One of these bugs is with Arrays in v8. When an array length is modified by some native array method (like push) the internal code does length = length + 1, which is not intercepted by Proxies, and the external interceptible code does length = length somewhy. This bug is the main reason why I listen on non value changing operations too. In the future it can be removed.

I hope it made sense. Sorry for the long comment and thx for the issue.