2016-03-21 4 views
13

私は今2週間RxJSを実験していますが、原則としてそれを愛していますが、状態を管理するための正しいパターンを見つけて実装できないようです。すべての記事と質問に同意するように見えます。簡単なRxJSの例題で、件名や必須の操作を使わずに状態を管理する方法は?

  • Subject可能な場合は、変換を経由して状態をプッシュすることをお勧めします。
  • .getValue()は完全に非推奨です。
  • .do DOM操作を除いて、おそらく避けるべきですか?

このようなすべての提案の問題は、「あなたはRxの方法を学び、Subjectを使うのをやめる」以外に、あなたが何を使うべきかを直接指摘するような文献はないということです。

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

は、覆われていない文献の問題点は以下のとおりです。

  • はじめは、あなたが行方不明になってきたプログラミングに反応する:偉大な出発テキストを、が、特にこれらの質問には対応していません。
  • RxJSのTODOの例にはReactが付属しており、React StoresのプロキシとしてSubjectを明示的に操作する必要があります。
  • http://blog.edanschwartz.com/2015/09/18/dead-simple-rxjs-todo-list/:アイテムの追加と削除に明示的にstateオブジェクトを使用します。

標準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$で、私の第二の試みに近づく方法

しかし、これはすでにだった:

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

明白な解決策は、またはconst items = new BehaviorSubject([])だけitems = []を持っており、それを直接操作することです - しかし、それを反復する唯一の方法は、getValueを使用して前の状態を公開しているように見えます。Andre Stalz(CycleJS)はRxJSの問題に、実際には公開すべきではないものとしてコメントしていますどのように使えますか?)。

私はちょうどストリームでは、あなたは主題を使ったり、何かを州のミートボールで表現したりしないという考えを持っていたと思います。最初の答えでは、孤立している/無限に成長している/互いに正確な順序で構築されなければならない連鎖したストリーム。

答えて

11

あなたはすでに良い例を見つけたと思います:http://jsbin.com/redeko/edit?js,output

あなたはこの実装

が明示的に追加し、アイテムを除去するための状態オブジェクトを使用するという事実の問題を取ります。

しかし、これはまさにあなたが探している良い練習です。たとえば、その状態オブジェクトの名前をviewModelに変更した場合は、より明白な可能性があります。

状態とは何ですか?

あり、他の定義があることが、私は次のような状態を考えるのが好きでしょう:

  • はすなわちoutput = f(input)、あなたが同じ入力に対して異なる出力を持つことができるよう、状態はに関連し、fに不純な機能を与えられましたその関数(存在する場合)はf(input) = output = g(input, state)が保持され、gが純関数であるような余分な変数です。

は、もしそうならば、ここでの機能は、TODOの配列に、ユーザの入力を表すオブジェクトを一致させることであり、すでに2つのドスを持っていると私はToDoリストにaddをクリックすると、出力は3トドスになります。 1つのToDoだけでToDoリストに同じ(同じ入力)を行うと、出力は2つのToDoになります。同じ入力、異なる出力。

ここで、その関数を純関数に変換できる状態は、todo配列の現在の値です。したがって、私の入力はaddのクリックの現在のtodo配列になり、新しいtodoリストを持つ新しいtodo配列を与える関数gを渡します。その関数gは純粋です。したがって、fは、以前は隠された状態をgで明示的にすることによって、ステートレスな方法で実装されています。

これは、純関数の作成を中心とする関数型プログラミングに適しています。それは他のRxJSかで、状態管理に来るとき、オペレータ

  • スキャン

だから、良い練習がそれを操作する状態を明示することです

Rxjs。

output = g(input, state)をストリームに変換すると、On+1 = g(In+1, Sn)となり、それはscanの演算子とまったく同じです。

  • scanを一般別のオペレータがexpandですが、これまでのところ、私はその作業をほとんど使用していた

を展開します。 scanは一般的にそのトリックを行います。

申し訳ありませんが長く、数学的な答えです。それらのコンセプトを克服するのにはしばらく時間がかかりました。それが私の理解できるようにしました。うまくいけば、それもあなたのために働く。

+0

これは本当に役に立ちますが、それでも2つの特定の問題に関してはまだありません...私の元の質問を改訂するだけです。あなたの更新に関する – brokenalarms

+0

、あなたの実装は正常です。実装したときに 'Current_Todos = Accumulated_Added_Todos - Removed_Todos'を持つことができますが、' Current_Todos'は有限であるが、Accumulated_Added_Todosは無限に大きくなる可能性があります。最も効率的な方法は、実際には、典型的なCRUD操作の1つで、Todos_n + 1 = Operations_n + 1(Todos_n)と書くことです。この場合、g(In + 1、Sn)= In + 1(Sn)となる。しかし、どちらの実装もステートレスです。 – user3743222

+0

アイデアを頭で囲むのに時間がかかるかもしれませんが、 'scan'の入力ストリームは現在の状態に適用して更新された状態を取得する関数(操作)のストリームです。 – user3743222

関連する問題