tc39 / proposal-observable

Observables for ECMAScript
https://tc39.github.io/proposal-observable/
3.06k stars 90 forks source link

Computed observables #125

Closed SystemParadox closed 7 years ago

SystemParadox commented 7 years ago

Forgive me if I have missed something obvious, but is there any consideration in the spec for "computed" observables? Or any way to globally watch for reading of observables?

For example, in knockout you can do something like this:

var a = ko.observable(1);
var b = ko.observable(2);
var c = ko.computed(function () {
    return a() + b();
});

c itself is an observable. When it is created, the function is run to get the initial value. c will automatically subscribe to any other observables read during the execution of the function. If any of its dependencies change, the function will be re-run and c will fire a change event to its subscribers if its value changes.

For more information see knockout's computed observables.

I find this feature incredibly useful. I have even re-implemented it in my own internal observable systems. But the problem is always interoperability. If you're using two observable libraries, they don't cooperate - computed functions won't subscribe to observables from another library because they can't detect when they have been read. I was really hoping that a standard observable would fix this problem.

Thanks.

RangerMauve commented 7 years ago

The spec doesn't come with any operators out of the box, but most implementations have something called a combineLatest operator which takes a list of observables, and a function which will get called with the latest results of all those observables every time one of them changes, and uses the return value of the function to pass down to subscribers. Here are some example docs: Rx.JS, Most, xstream

SystemParadox commented 7 years ago

No that's not what I'm looking for at all. That approach requires manually passing in all the required observables, which is error-prone and horrendous to maintain.

All we need is a way to monitor which observables have been read and we can subscribe to them automatically. KnockoutJS supports this, as does MobX. The lack of support for this is one of the main reasons I have avoided RxJS.

Without this, the only way to automatically watch dependencies of an expression is to create a custom expression language, parse it, evaluate the required observables and subscribe to them that way. Why create a custom language when we can just do it in JS?

RangerMauve commented 7 years ago

I doubt that this is going to land in the core observable spec any time soon. The main reason is that it requires a framework to track all observable instances in order to detect which ones are being invoked, as you can see in the Knockout docs. This means that all observable instances will need to be more stateful and could introduce performance hits and memory leaks, which as far as I know is something the spec authors have been very weary of during the design process.

However, since the spec provides interoperability between observables, you could convert an observable from a library that doesn't support computed observables to one that does. So, for example, you can consume DOM-based observables (when they eventually arrive in the browser) or Rx observables, and then convert them to Knockout, which can then use the standard subscription API, in addition to its computed observable magic to let you use any observable you want.

appsforartists commented 7 years ago

This is definitely something you can write on-top of Observables without much difficulty.

Write a higher-order function that checks if any of its inputs have a subscribe method. If not, store their present value locally. If so, call that subscribe method and update your argument cache when an argument dispatches a new value.

When all your arguments have a value, call the inner function and dispatch its result. Repeat this any time one of the inputs change.

I've prototyped something similar in both RxJS and xstream. Here's the xstream version. This one makes operators, but there's no reason you couldn't do it as a standalone function.

RangerMauve commented 7 years ago

@appsforartists I think @SystemParadox doesn't want to have to pass a reference to the Observables to the operator. They want to have the framework automatically detect dependencies by invoking the listener function and seeing which observables get touched.

benjamingr commented 7 years ago

The purpose of the spec is to define minimal glue for libraries and native APIs to use with observables.

What you're looking for is better accomplished in userland libraries like MobX and they integrate through Symbol.observable.

appsforartists commented 7 years ago

I'm skeptical of magic to begin with, but especially in an otherwise-unmagic TC39 spec. I vote we close this issue.

benjamingr commented 7 years ago

This is not magic, it's incredibly simple to reason about or write just the data binder part of Knockout like KO does , it's just not the right place to do so.

appsforartists commented 7 years ago

Maybe I misunderstood. I thought the request was for a function to know if any variables it uses are observables and to rerun any time one dispatches.

mattpodwysocki commented 7 years ago

I'd agree this is outside the scope of this proposal, so closing the issue.

SystemParadox commented 7 years ago

It seems that my suggestion doesn't really make sense with the proposed observables because they aren't stateful. They don't keep track of their current value, so there's no "read" method for computed observables to hook into. ES observables are based on sequences rather than individual values and are more akin to event emitters or streams than they are to Knockout observables.