Wie verwalte ich den Status in einem einfachen RxJS-Beispiel ohne Verwendung von Subject oder imperativer Manipulation?

Ich experimentiere jetzt seit zwei Wochen mit RxJS, und obwohl ich es im Prinzip liebe, kann ich einfach nicht das richtige Muster für die Verwaltung des Zustands finden und implementieren. Alle Artikel und Fragen scheinen zu stimmen:

Subject sollte nach Möglichkeit vermieden werden, um den Zustand nur über Transformationen durchzusetzen;.getValue() sollte vollständig veraltet sein; un.do sollte außer DOM-Manipulation vielleicht vermieden werden?

Das Problem bei all diesen Vorschlägen ist, dass keine der Literaturstellen direkt sagt, was Sie stattdessen verwenden sollten, außer "Sie werden den Rx-Weg erlernen und aufhören, Subject zu verwenden".

Aber ich kann nirgendwo ein direktes Beispiel finden, das genau angibt, wie ein einzelner Stream / ein einzelnes Objekt auf zustandslose und funktionale Weise hinzugefügt und entfernt werden kann.

Bevor ich wieder in die gleichen Richtungen weise, sind Probleme mit ungedeckter Literatur:

Die Einführung in die reaktive Programmierung Sie haben gefehlt: ein großartiger Ausgangstext, der diese Fragen jedoch nicht speziell behandelt.Das TODO-Beispiel für RxJS wird mit React geliefert und beinhaltet die explizite Manipulation vonSubjects als Proxys für React Stores.http: //blog.edanschwartz.com/2015/09/18/dead-simple-rxjs-todo-list: verwendet explizit einstate Objekt zum Hinzufügen und Entfernen von Elementen.

Meine vielleicht 10. Neufassung des Standard-TODO folgt - Meine vorherigen Iterationen enthalten:

Starten mit einem veränderlichen 'items'-Array - schlecht, da der Status explizit und zwingend verwaltet wird usingscan, um neue Elemente mit einem @ zu verknüpfaddedItems$ stream, dann Verzweigen eines anderen Streams, in dem die entfernten Elemente gelöscht wurden - schlecht wie dasaddedItems$ Stream würde auf unbestimmte Zeit wachsen.entdeckenBehaviorSubjectund mit dem - schien schlecht, da für jedes neueupdatedList$.next()ei der Emission muss der vorherige Wert iteriert werden, was bedeutet, dassSubject.getValue() ist bedeutsamVersuche das Ergebnis des @ zu streaminputEnter$ Ereignisse zu gefilterten Entfernungsereignissen hinzufügen - aber dann erstellt jeder neue Stream eine neue Liste und fügt diese dann in das @ eitoggleItem$ undtoggleAll$ streams bedeutet, dass jeder neue Stream vom vorherigen Stream abhängig ist. Wenn Sie also eine der 4 Aktionen (Hinzufügen, Entfernen, Umschalten von Elementen oder Umschalten aller) ausführen, muss die gesamte Kette unnötigerweise erneut durchlaufen werden.

Nun habe ich den Kreis geschlossen, wo ich wieder beides benutzeSubject (und wie soll es in irgendeiner Weise nacheinander durchlaufen werden, ohne @ zu verwendegetValue()?) unddo, wie unten gezeigt. Ich und mein Kollege sind uns einig, dass dies der klarste Weg ist, aber es scheint natürlich der am wenigsten reaktive und zwingendste zu sein. Für klare Vorschläge zum richtigen Weg wäre ich sehr dankbar!

import Rx from 'rxjs/Rx';
import h from 'virtual-dom/h';
import diff from 'virtual-dom/diff';
import patch from 'virtual-dom/patch';

const todoListContainer = document.querySelector('#todo-items-container');
const newTodoInput = document.querySelector('#new-todo');
const todoMain = document.querySelector('#main');
const todoFooter = document.querySelector('#footer');
const inputToggleAll = document.querySelector('#toggle-all');
const ENTER_KEY = 13;

// INTENTS
const inputEnter$ = Rx.Observable.fromEvent(newTodoInput, 'keyup')
    .filter(event => event.keyCode === ENTER_KEY)
    .map(event => event.target.value)
    .filter(value => value.trim().length)
    .map(value => {
        return { label: value, completed: false };
    });

const inputItemClick$ = Rx.Observable.fromEvent(todoListContainer, 'click');

const inputToggleAll$ = Rx.Observable.fromEvent(inputToggleAll, 'click')
    .map(event => event.target.checked);

const inputToggleItem$ = inputItemClick$
    .filter(event => event.target.classList.contains('toggle'))
    .map((event) => {
        return {
            label: event.target.nextElementSibling.innerText.trim(),
            completed: event.target.checked,
        };
    })

const inputDoubleClick$ = Rx.Observable.fromEvent(todoListContainer, 'dblclick')
    .filter(event => event.target.tagName === 'LABEL')
    .do((event) => {
        event.target.parentElement.classList.toggle('editing');
    })
    .map(event => event.target.innerText.trim());

const inputClickDelete$ = inputItemClick$
    .filter(event => event.target.classList.contains('destroy'))
    .map((event) => {
        return { label: event.target.previousElementSibling.innerText.trim(), completed: false };
    });

const list$ = new Rx.BehaviorSubject([]);

// MODEL / OPERATIONS
const addItem$ = inputEnter$
    .do((item) => {
        inputToggleAll.checked = false;
        list$.next(list$.getValue().concat(item));
    });

const removeItem$ = inputClickDelete$
    .do((removeItem) => {
        list$.next(list$.getValue().filter(item => item.label !== removeItem.label));
    });

const toggleAll$ = inputToggleAll$
    .do((allComplete) => {
        list$.next(toggleAllComplete(list$.getValue(), allComplete));
    });

function toggleAllComplete(arr, allComplete) {
    inputToggleAll.checked = allComplete;
    return arr.map((item) =>
        ({ label: item.label, completed: allComplete }));
}

const toggleItem$ = inputToggleItem$
    .do((toggleItem) => {
        let allComplete = toggleItem.completed;
        let noneComplete = !toggleItem.completed;
        const list = list$.getValue().map(item => {
            if (item.label === toggleItem.label) {
                item.completed = toggleItem.completed;
            }
            if (allComplete && !item.completed) {
                allComplete = false;
            }
            if (noneComplete && item.completed) {
                noneComplete = false;
            }
            return item;
        });
        if (allComplete) {
            list$.next(toggleAllComplete(list, true));
            return;
        }
        if (noneComplete) {
            list$.next(toggleAllComplete(list, false));
            return;
        }
        list$.next(list);
    });

// subscribe to all the events that cause the proxy list$ subject array to be updated
Rx.Observable.merge(addItem$, removeItem$, toggleAll$, toggleItem$).subscribe();

list$.subscribe((list) => {
    // DOM side-effects based on list size
    todoFooter.style.visibility = todoMain.style.visibility =
        (list.length) ? 'visible' : 'hidden';
    newTodoInput.value = '';
});

// RENDERING
const tree$ = list$
    .map(newList => renderList(newList));

const patches$ = tree$
    .bufferCount(2, 1)
    .map(([oldTree, newTree]) => diff(oldTree, newTree));

const todoList$ = patches$.startWith(document.querySelector('#todo-list'))
    .scan((rootNode, patches) => patch(rootNode, patches));

todoList$.subscribe();


function renderList(arr, allComplete) {
    return h('ul#todo-list', arr.map(val =>
        h('li', {
            className: (val.completed) ? 'completed' : null,
        }, [h('input', {
                className: 'toggle',
                type: 'checkbox',
                checked: val.completed,
            }), h('label', val.label),
            h('button', { className: 'destroy' }),
        ])));
}
Bearbeite

In Bezug auf @ user3743222 sehr hilfreiche Antwort, ich kann sehen, wie das Darstellen von Zustand als zusätzliche Eingabe eine Funktion rein machen kann und somitscan ist der beste Weg, um eine Sammlung, die sich im Laufe der Zeit entwickelt, mit einer Momentaufnahme ihres vorherigen Zustands bis zu diesem Zeitpunkt als zusätzlichen Funktionsparameter darzustellen.

Aber so bin ich schon meinen zweiten Versuch angegangen, mitaddedItems$ wird ein gescannter Strom von Eingaben:

// this list will now grow infinitely, because nothing is ever removed from it at the same time as concatenation?
const listWithItemsAdded$ = inputEnter$
    .startWith([])
    .scan((list, addItem) => list.concat(addItem));

const listWithItemsAddedAndRemoved$ = inputClickDelete$.withLatestFrom(listWithItemsAdded$)
    .scan((list, removeItem) => list.filter(item => item !== removeItem));

// Now I have to always work from the previous list, to get the incorporated amendments...
const listWithItemsAddedAndRemovedAndToggled$ = inputToggleItem$.withLatestFrom(listWithItemsAddedAndRemoved$)
    .map((item, list) => {
        if (item.checked === true) {
        //etc
        }
    })
    // ... and have the event triggering a bunch of previous inputs it may have nothing to do with.


// and so if I have 400 inputs it appears at this stage to still run all the previous functions every time -any- input
// changes, even if I just want to change one small part of state
const n$ = nminus1$.scan...

ie naheliegende Lösung wäre, einfach @ zu habitems = [], und manipulieren Sie es direkt, oderconst items = new BehaviorSubject([]) - aber dann scheint die einzige Möglichkeit, darauf zu iterieren, die Verwendung von @ zu segetValue um den vorherigen Zustand aufzudecken, den Andre Stalz (CycleJS) in den RxJS-Ausgaben als etwas kommentiert hat, das eigentlich nicht aufgedeckt werden sollte (aber wenn nicht, wie ist es dann verwendbar?).

Ich glaube, ich hatte gerade die Idee, dass Sie bei Streams keine Subjects verwenden oder irgendetwas über einen staatlichen 'Fleischbällchen' darstellen sollten, und bei der ersten Antwort bin ich mir nicht sicher, wie dies keine massengeketteten Streams einführt, die verwaist sind / unendlich wachsen / in exakter Reihenfolge aufeinander aufbauen müssen.

Antworten auf die Frage(2)

Ihre Antwort auf die Frage