javascript - Subiect RxJS partajat pentru legarea bidirecțională a datelor în Angular 2


Am un serviciu singleton pentru setările aplicației

class Setting {
  get foo() {
    return storage.get('foo');
  }

  set foo(val)
    storage.set('foo', val);
  }
}

aceasta este legată în vizualizările componentelor ca setting.foo.

Deoarece apelurile storage pot fi costisitoare și pentru că pot fi asincrone, aș prefera să înlocuiesc getter/setter cu un subiect RxJS care ar putea actualiza și citi storage ori de câte ori este nevoie.

Așa că îl restructurez pentru a

class Setting {

  constructor() {
    this.fooSubject = new ReplaySubject(1);

    fooSubject.subscribe((val) => {
      storage.set('foo', val);
    });

    this.foo$ = this.fooSubject
    .map(() => storage.get('foo'))
    .publishReplay(1).refCount();
}

și folosindu-l astfel setting.foo$ | async și setting.fooSubject.next(newFoo). Se pare că apelurile costisitoare storage.get sunt acum puse în cache.

Există două probleme.

Primul este că atât subiectul fooSubject, cât și observabilul foo$ ar trebui să fie disponibile public pentru a face acest lucru să funcționeze, în timp ce a fost ales un subiect pentru că se presupune că este atât un observabil, cât și un observator.

Poate foo$ să fie făcută o singură proprietate Subject a serviciului Setting, astfel încât să poată fi abonată cu subscribe(...) și actualizată cu next(...)?

Al doilea este că codul este încă sincron.

Cum poate fi tratat acest caz pentru storage.get și storage.set care returnează promisiuni?

Răspuns #1

Nu există prea multe de sugerat în codul tău atunci când funcționează exact așa cum vrei. Nu sunt sigur că înțeleg exact cazul tău de utilizare, dar cred că poți să-ți simplifici codul nefolosind deloc this.foo$.

Funcționalitatea de a stoca cea mai recentă valoare este oferită de ReplaySubject și, cu excepția cazului în care aveți cu adevărat nevoie să apelați storage.get('foo') de fiecare dată după storage.set('foo', val);, nu este necesar.

Am pus codul tău într-un demo live: http://plnkr.co/edit/pxjRQr6Q6Q7LzYb1oode?p=preview

Deci cred că poate fi simplificat astfel.

class Setting {

  constructor() {
    var storage = new Storage();

    this.fooSubject = new ReplaySubject(1);
    this.fooSubject.subscribe((val) => {
      storage.set('foo', val);
    });
  }

  get observable() {
    return this.fooSubject.asObservable();
  };

  store(val) {
    this.fooSubject.next(val);
  }
}

Ascund în mod voit faptul că folosesc un Subject cu .asObservable() și prin învelirea apelului .next() cu metoda store(). O utilizare tipică ar putea arăta astfel:

let settings = new Setting();

settings.store('Hello');

settings.observable.subscribe(val => console.log(val));
settings.store('Hello 2');
settings.store('Hello 3');

settings.observable.subscribe(val => console.log(val));
settings.store('Hello 4');

Care afișează în consolă:

Hello
Hello 2
Hello 3
Hello 3
Hello 4
Hello 4

Observați că nu inițializați ReplaySubject cu nicio valoare. Chiar dacă ați apelat setting.fooSubject.next(newFoo) imediat după crearea ReplaySubject, aceasta va fi stocată din nou imediat după abonare cu storage.set('foo', val);.

Problema legată de a fi sincron. Ei bine, codul tău este de fapt asincron, dar secvențial. Deoarece JavaScript este single-threaded, atunci dacă storage.get('foo') face o operațiune sincronă care consumă timp, atunci va bloca firul de execuție și probabil singura opțiune este să-l muți într-un Web Worker.

Răspuns #2

A fost foarte ușor să obții un subiect cu efecte secundare prin extinderea AnonymousSubject (o clasă pentru fabrica Subject.create(...)). Subiectul rezultat primește proprietățile destination și source care păstrează subiectul original și observabilul.

class FooSharedSubject extends AnonymousSubject {
    constructor() {
        const subject = new BehaviorSubject('');

        const observable = subject.asObservable()
        .mergeMap((value) => promisedStorage.get('foo'))
        .publishReplay(1)
        .refCount();

        super(subject, observable);
    }

    next(value): void {
        promisedStorage.set('foo', value)).then(
            () => this.destination.next(value),
            () => this.destination.error(value)
        );
    }
}