我已经尝试了RxJS两周了,虽然我原则上喜欢它,但我似乎无法找到并实现正确的状态管理模式。所有文章和问题似乎都同意:
- 应尽可能避免
Subject
,而只是通过转换来推动状态; -
.getValue()
应该完全弃用;和 - 除了 DOM 操作之外,也许应该避免
.do
?
所有这些建议的问题在于,除了"您将学习Rx方式并停止使用Subject"之外,似乎没有文献直接说明您应该使用什么。
但是我在任何地方都找不到一个直接的例子,专门指示以无状态和功能性方式对单个流/对象执行添加和删除的正确方法,这是多个其他流输入的结果。
在我再次被指向相同的方向之前,未发现的文献的问题是:
- 响应式编程简介 你一直缺少:很棒的起始文本,但没有具体解决这些问题。
- RxJS 的 TODO 示例随 React 一起提供,涉及显式操作
Subject
作为 React Store 的代理。 - http://blog.edanschwartz.com/2015/09/18/dead-simple-rxjs-todo-list/:显式使用
state
对象来添加和删除项目。
以下是我对标准 TODO 的第 10 次重写 - 我之前涵盖的迭代包括:
- 从一个可变的"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
来公开以前的状态,Andre Stalz(CycleJS)在RxJS问题中评论说,不应该真正公开(但同样, 如果没有,那么它如何可用?
我想我只是有一个想法,对于流,你不应该使用主题或通过状态"肉丸"表示任何东西,在第一个答案中,我不确定这如何不引入大量链式流,这些流是孤立的/无限增长/必须以精确的顺序相互构建。
我想你已经找到了一个很好的例子: http://jsbin.com/redeko/edit?js,output。
您对这种实现这一事实提出异议
显式使用 State 对象来添加和删除项。
但是,这正是您正在寻找的良好做法。例如,如果将该状态对象重命名viewModel
,则可能对您来说更明显。
那么什么是国家呢?
会有其他定义,但我喜欢将状态考虑如下:
- 给定
f
不纯函数,即output = f(input)
,这样你可以对同一个输入有不同的输出,与该函数关联的状态(如果存在)是额外的变量,这样f(input) = output = g(input, state)
成立,g 是一个纯函数。
因此,如果这里的功能是将表示用户输入的对象与待办事项数组匹配,并且如果我单击已经有 2 个待办事项的待办事项列表上的add
,则输出将是 3 个待办事项。如果我在只有一个待办事项的待办事项列表中执行相同的操作(相同的输入),则输出将是 2 个待办事项。所以相同的输入,不同的输出。
这里允许将该函数转换为纯函数的状态是待办事项数组的当前值。所以我的输入变成了一个add
点击,以及当前的待办事项数组,通过一个函数g
传递,该函数给出了一个新的待办事项数组和一个新的待办事项列表。该函数 g 是纯函数。因此,f
是通过在g
中明确其先前隐藏的状态以无状态方式实现的。
这很符合围绕组合纯函数的函数式编程。
Rxjs 运算符
- 扫描
因此,当涉及到状态管理时,无论是使用 RxJS 还是其他方式,一个好的做法是显式地使状态来操纵它。
如果将output = g(input, state)
转换为流,则会得到On+1 = g(In+1, Sn)
,这正是scan
运算符所做的。
- 扩大
另一个概括scan
运算符是expand
,但到目前为止,我很少使用该运算符。 scan
通常可以解决问题。
对不起,冗长而数学的答案。我花了一段时间才绕过这些概念,这就是我让它们对我来说易于理解的方式。希望它也适合您。