15

私は RxJS を 2 週間実験してきましたが、基本的には気に入っていますが、状態を管理するための正しいパターンを見つけて実装することができないようです。すべての記事と質問は一致しているように見えます:

  • Subject変換を介して状態を単にプッシュすることを支持して、可能な限り避けるべきです。
  • .getValue()完全に非推奨にする必要があります。と
  • .doDOM操作を除いて、おそらく避けるべきですか?

このようなすべての提案の問題は、「Rx の方法を学び、Subject の使用をやめる」以外に、代わりに何を使用すべきかを直接述べている文献がないように見えることです。

しかし、ステートレスで機能的な方法で、他の複数のストリーム入力の結果として、単一のストリーム/オブジェクトへの追加と削除の両方を実行する正しい方法を具体的に示す直接的な例はどこにも見つかりません。

再び同じ方向性を指摘する前に、未公開の文献の問題点は次のとおりです。

  • リアクティブ プログラミング入門 見落としていました: すばらしい入門テキストですが、これらの質問に具体的には触れていません。
  • RxJS の TODO の例は React に付属しておりSubject、React ストアのプロキシとして s を明示的に操作する必要があります。
  • http://blog.edanschwartz.com/2015/09/18/dead-simple-rxjs-todo-list/state :アイテムの追加と削除にオブジェクトを明示的に使用します。

私のおそらく 10 回目の標準 TODO の書き直しは次のとおりです。

  • 変更可能な「アイテム」配列で始まる - 状態が明示的で命令的に管理されているため悪い
  • scan新しいアイテムをストリームに連結するために使用してからaddedItems$、削除されたアイテムが削除された別のストリームを分岐します -addedItems$ストリームが無限に大きくなるので悪いです。
  • それを発見BehaviorSubjectして使用することは、新しい放出ごとupdatedList$.next()に以前の値を反復する必要があるため、悪いように思えました。つまり、それSubject.getValue()が不可欠です。
  • 追加イベントの結果をフィルタリングされた削除イベントにストリーミングしようとしますinputEnter$が、新しいストリームごとに新しいリストが作成され、それをストリームにフィードするとtoggleItem$toggleAll$新しいストリームはそれぞれ前のストリームに依存することになり、4 つのうちの 1 つが発生します。アクション (追加、削除、アイテムの切り替え、またはすべての切り替え) では、チェーン全体を不必要に再実行する必要があります。

今、私は完全に一周し、以下に示すように、 と の両方を使用することに戻りましたSubject(そして、使用せずにどのように連続して反復する必要があるのgetValue()でしょうか?) do。私自身も同僚も、これが最も明確な方法であることに同意しますが、もちろん、これは最も反応が少なく、最も必須の方法のように思えます。これに対する正しい方法に関する明確な提案は大歓迎です!

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' }),
        ])));
}

編集

@ user3743222の非常に役立つ回答に関連して、追加の入力として状態を表すと関数が純粋になり、scanその時点までの以前の状態のスナップショットを使用して、時間の経過とともに進化するコレクションを表す最良の方法であることがわかります。追加の関数パラメーター。

addedItems$ただし、これは、入力のスキャンされたストリームを使用して、2 回目の試みにアプローチした方法です。

// 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...

明らかな解決策はitems = []、 を持って直接操作するか、またはconst items = new BehaviorSubject([])- を反復する唯一の方法はgetValue、以前の状態を公開するために を使用することです.それは実際には公開されるべきではありません (ただし、公開されていない場合、どのように使用できるのでしょうか?)。

ストリームでは、サブジェクトを使用したり、状態「ミートボール」を介して何かを表したりすることは想定されていないという考えがあったと思います。最初の回答では、これがマスチェーンストリームを導入しない方法がわかりません孤児/無限に成長/正確な順序で互いに構築する必要があります。

4

1 に答える 1