我已经开始尝试使用 RxJS 两周了,虽然我原则上喜欢它,但似乎找不到正确的状态管理模式并加以实现。所有文章和问题都似乎达成了以下共识:
- 可能情况下应避免使用
Subject
,而是通过变换来推送状态。 - 完全弃用
.getValue()
; - 除了 DOM 操作之外,也许应避免使用
.do
?
所有这些建议的问题在于,除了“你会学习 Rx 的方式并停止使用 Subject”之外,没有任何文献直接说明你应该使用什么替代方案。
但我无法找到任何明确的示例,特别指出以无状态和函数式方式从多个其他流输入导致单个流/对象的添加和删除的正确方法。
在被指向相同方向之前,我想指出未涵盖的文献问题:
- 《Reactive Programming Introduction》:很好的入门文本,但不具体解答这些问题。
- React 自带的 TODO 示例涉及显式操作
Subject
作为 React Store 代理。 - http://blog.edanschwartz.com/2015/09/18/dead-simple-rxjs-todo-list/:明确使用
state
对象添加和删除项目。
接下来是我第十次重写标准 TODO - 在此之前,我的尝试包括:
- 从可变的“items”数组开始-不好,因为状态是显式和命令式管理的
- 使用
scan
将新项目连接到addedItems$
流中,然后分叉另一个流,在其中删除已删除的项目-不好,因为addedItems$
流会无限增长。 - 发现
BehaviorSubject
并使用它-似乎不好,因为对于每个新的updatedList$.next()
发射,它都需要迭代先前的值,这意味着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
暴露先前状态,这在 RxJS 的问题中被 Andre Stalz (CycleJS) 评论为不应该被公开的内容(但如果没有,那么它如何可用?)。我想我只是想到了一个关于流的想法,即您不应该使用 Subjects 或通过状态“肉球”来表示任何东西,并且在第一个答案中,我不确定这如何不会引入大量链接的流,这些流是孤立的/无限增长/必须按照精确顺序构建彼此。
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)
。但我认为这两种实现都是无状态的。 - user3743222scan
输入流是一系列函数(操作)的流,你可以将它们应用于当前状态以获取更新后的状态。 - user3743222