Я уже два недели экспериментирую с 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 как нечто, что на самом деле не должно быть открыто (но опять же, если нет, то как это можно использовать?).
Я предполагаю, что у меня была идея, что с потоками вы не должны были использовать Субъекты или представлять что-либо через состояние «фрикадельки», и в первом ответе я не уверен, как это не вводит массу цепные потоки, осиротевшие/растущие бесконечно/должны строить друг на друга в точном порядке.
Это действительно полезно, но все еще не совсем оно имеет отношение к двум конкретным вопросам ... просто пересматривая мой первоначальный вопрос. – brokenalarms
относительно ваших обновлений, ваша реализация в порядке. У вас может быть '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
Может потребоваться некоторое время, чтобы обернуть вокруг себя идею, но ваш поток ввода для 'scan' - это поток функций (операций), которые вы применяете к текущему состоянию, чтобы получить обновленное состояние. – user3743222