2016-03-21 17 views
13

Я уже два недели экспериментирую с RxJS, и, хотя мне это нравится, я просто не могу найти и реализовать правильный шаблон для управления состоянием. Все статьи и вопросы, кажется, соглашаются:Как управлять состоянием без использования темы или императивной манипуляции в простом примере RxJS?

  • Subject следует избегать, где это возможно, в пользу просто толкая состояние через с помощью преобразований;
  • .getValue() должно быть полностью отменено; и
  • .do следует избегать, за исключением манипулирования DOM?

Проблема со всеми такими предложениями заключается в том, что ни одна из литературы, по-видимому, прямо не говорит, что вы должны использовать вместо этого, кроме «вы узнаете путь Rx и прекратите использование темы».

Но я не могу найти прямой пример в любом месте, где конкретно указывается правильный способ выполнения как добавлений, так и абсорбции для одного потока/объекта, как следствие нескольких других потоков, без учета состояния и функционала.

Прежде чем я снова указал в тех же направлениях, проблемы с непокрытой литературы:

  • Введение в Реактивная Программирование Вы не хватало: большой стартовый текст, но не конкретно эти вопросы.
  • Пример TODO для RxJS поставляется с React и предполагает явное манипулирование Subject s в качестве прокси-серверов для React Stores.
  • http://blog.edanschwartz.com/2015/09/18/dead-simple-rxjs-todo-list/: явно использует объект state для добавления и удаления предметов.

Мой возможно десятый переписывает стандартный TODO следующим образом: - Мои предыдущие итерации охватывают:

  • начиная с массивом изменяемых «пунктами» - плохо, как состояние явно и властно удалось
  • использования scan чтобы объединить новые элементы в поток addedItems$, а затем разветвить другой поток, в котором удаленные элементы были удалены - плохо, поскольку поток addedItems$ будет расти бесконечно.
  • обнаружение BehaviorSubject и использование этого - казалось плохим, поскольку для каждого нового излучения требуется, чтобы предыдущее значение повторялось, что означает, что Subject.getValue() является существенным.
  • пытается передать результат событий inputEnter$ в отфильтрованные события удаления, но затем каждый новый поток создает новый список, а затем его подачу в потоки toggleItem$ и toggleAll$ означает, что каждый новый поток зависит от предыдущего и поэтому включение одного из 4-х действий (добавление, удаление, переключение элемента или переключения всех) требует, чтобы вся цепочка была излишне повторена.

Теперь я пришел полный круг, где я вернулся к использованию как 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... 

Очевидным решением было бы просто items = [] и манипулировать ею напрямую, или const items = new BehaviorSubject([]) - но тогда единственный способ перебора на нем, по-видимому, использует getValue, чтобы показать предыдущее состояние, которое Андре Стальц (CycleJS) прокомментировал в проблемах RxJS как нечто, что на самом деле не должно быть открыто (но опять же, если нет, то как это можно использовать?).

Я предполагаю, что у меня была идея, что с потоками вы не должны были использовать Субъекты или представлять что-либо через состояние «фрикадельки», и в первом ответе я не уверен, как это не вводит массу цепные потоки, осиротевшие/растущие бесконечно/должны строить друг на друга в точном порядке.

ответ

11

Я думаю, вы уже нашли хороший пример: http://jsbin.com/redeko/edit?js,output.

Вы не согласны с тем, что эта реализация

явно использует объект состояния для добавления и удаления элементов.

Однако, это именно то, что вы ищете. Например, если вы переименуете этот объект состояния viewModel, это может быть более очевидно для вас.

Так что же такое состояние?

Там будут и другие определения, но мне нравится думать о состоянии следующим образом:

  • дал f нечистой функции, т.е. output = f(input), так что вы можете иметь различные выходы для того же вход, состояние, связанные с эта функция (когда она существует) является дополнительной переменной, такой, что имеет место f(input) = output = g(input, state), а g - чистая функция.

Таким образом, если функция здесь, чтобы соответствовать объект, представляющий пользовательский ввод в массив TODO, и если я нажимаю add на список задач с уже 2 ToDos, выход будет 3 Todos. Если я сделаю то же самое (тот же ввод) в списке задач с одним только одним, выход будет 2 todos. Такой же вход, разные выходы.

Состояние, которое позволяет преобразовать эту функцию в чистую функцию, является текущим значением массива todo. Таким образом, мой ввод становится add щелчком, И текущий массив todo, прошедший через функцию g, которые дают новый массив todo с новым списком todo. Эта функция g чиста.Таким образом, f реализован без апатридов, сделав свое ранее скрытое состояние явным в g.

И это хорошо сочетается с функциональным программированием, которое вращается вокруг создания чистых функций.

Rxjs операторы

  • сканирования

Так что, когда дело доходит до государственного управления, с RxJS или же, хорошая практика, чтобы сделать государство явно манипулировать им.

Если вы включите output = g(input, state) в поток, вы получите On+1 = g(In+1, Sn), и это именно то, что делает оператор scan.

  • расширить

другой оператор, обобщающий scan является expand, но до сих пор у меня было очень мало пользы этого оператора. scan обычно делает трюк.

Извините за длинный и матовый ответ. Мне потребовалось некоторое время, чтобы обойти эти концепции, и именно так я сделал их понятными для меня. Надеюсь, это сработает и для вас.

+0

Это действительно полезно, но все еще не совсем оно имеет отношение к двум конкретным вопросам ... просто пересматривая мой первоначальный вопрос. – brokenalarms

+0

относительно ваших обновлений, ваша реализация в порядке. У вас может быть 'Current_Todos = Accumulated_Added_Todos - Removed_Todos', как вы это сделали, но проблема с этой реализацией, как вы сказали, заключается в том, что в то время как' Current_Todos' является конечным, 'Accumulated_Added_Todos' может расти мгновенно. Самый эффективный способ - написать Todos_n + 1 = Operations_n + 1 (Todos_n), с операциями одной из типичных операций CRUD. то есть g (In + 1, Sn) = In + 1 (Sn) 'в этом случае. Я думаю, что обе реализации являются апатридами. – user3743222

+0

Может потребоваться некоторое время, чтобы обернуть вокруг себя идею, но ваш поток ввода для 'scan' - это поток функций (операций), которые вы применяете к текущему состоянию, чтобы получить обновленное состояние. – user3743222

 Смежные вопросы

  • Нет связанных вопросов^_^