在一个简单的 RxJS 示例中,如何在不使用主题或命令式操作的情况下管理状态?
Posted
技术标签:
【中文标题】在一个简单的 RxJS 示例中,如何在不使用主题或命令式操作的情况下管理状态?【英文标题】:How to manage state without using Subject or imperative manipulation in a simple RxJS example? 【发布时间】:2016-07-08 13:35:36 【问题描述】:我已经用 RxJS 进行了两个星期的试验,虽然我原则上喜欢它,但我似乎无法找到并实施正确的状态管理模式。所有文章和问题似乎都同意:
应尽可能避免Subject
,而应仅通过转换推动状态;
.getValue()
应该被完全弃用;和
.do
除了 DOM 操作外,或许应该避免使用?
所有这些建议的问题在于,除了“你将学习 Rx 方式并停止使用 Subject”之外,似乎没有任何文献直接说明你应该使用什么。
但是,由于多个其他流输入的结果,以无状态和功能的方式,我无法在任何地方找到具体指示对单个流/对象执行添加和删除的正确方法的直接示例。
在我再次指出相同的方向之前,未发现文献的问题是:
您错过的响应式编程简介:很棒的入门文本,但并未具体解决这些问题。 RxJS 的 TODO 示例随 React 一起提供,涉及将Subject
s 作为 React 存储的代理进行显式操作。
http://blog.edanschwartz.com/2015/09/18/dead-simple-rxjs-todo-list/ :明确使用 state
对象来添加和删除项目。
接下来是我对标准 TODO 的第 10 次重写 - 我之前的迭代包括:
从一个可变的“项目”数组开始 - 不好,因为状态是显式的并且是命令式管理的 使用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 问题中评论说不应该真正暴露(但同样,如果没有,那么它如何使用?)。
我想我只是有一个想法,即使用流,您不应该使用主题或通过状态“肉丸”表示任何东西,在第一个答案中,我不确定这如何不引入质量链孤立/无限增长/必须以精确顺序相互建立的流。
【问题讨论】:
【参考方案1】:我想你已经找到了一个很好的例子:http://jsbin.com/redeko/edit?js,output。
你对这个实现有异议
明确使用状态对象来添加和删除项目。
但是,这正是您正在寻找的良好做法。例如,如果您将该状态对象重命名为 viewModel
,它可能对您来说更明显。
那么什么是状态?
会有其他定义,但我喜欢将状态视为如下:
给定f
一个不纯函数,即output = f(input)
,这样您就可以为相同的输入提供不同的输出,与该函数相关联的状态(如果存在)是额外的变量,使得f(input) = output = g(input, state)
成立并且g 是一个纯函数。
因此,如果这里的函数是将表示用户输入的对象与待办事项数组匹配,并且如果我在已经有 2 个待办事项的待办事项列表上单击 add
,则输出将是 3 个待办事项。如果我在只有一个待办事项的待办事项列表上执行相同(相同的输入),则输出将是 2 个待办事项。所以同样的输入,不同的输出。
这里允许将该函数转换为纯函数的状态是待办事项数组的当前值。所以我的输入变成了add
点击,AND 当前的待办事项数组,通过一个函数g
,它给出了一个新的待办事项数组和一个新的待办事项列表。那个函数 g 是纯的。所以f
是通过在g
中显式显示其先前隐藏的状态以无状态方式实现的。
这非常适合以组合纯函数为中心的函数式编程。
Rxjs 运算符
扫描因此,当涉及到状态管理时,无论是使用 RxJS 还是其他方式,一个好的做法是使状态显式地进行操作。
如果将output = g(input, state)
转换为流,则会得到On+1 = g(In+1, Sn)
,这正是scan
运算符所做的。
另一个泛化scan
的运算符是expand
,但到目前为止我很少使用该运算符。 scan
通常可以解决问题。
抱歉,答案冗长而数学化。我花了一段时间来解决这些概念,这就是我让它们对我来说可以理解的方式。希望它也对你有用。
【讨论】:
这确实很有帮助,但对于两个特定问题仍然不太了解......只是修改我原来的问题。 关于您的更新,您的实施很好。您可以在实现时拥有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)
在这种情况下。但我认为这两种实现都是无状态的。
您可能需要一些时间来理解这个想法,但是scan
的输入流是一个函数流(操作),您将其应用于当前状态以获得更新的状态.
这是一种通用的 GUI 设计模式。您的视图是模型的函数(此处为待办事项列表),例如View = getView(M)
。您从用户那里收到另一方的意图(添加、删除等),所以Intents = getIntent(Events)
,然后从您的意图中,您派生操作,这些操作采用模型并返回该模型的更新版本,您应用这些操作,getUpdatedModel(getAction(Intents))
,你不纯的getUpdatedModel
在这里用scan
实现。所以在明确的View = getView(getUpdatedModel(getAction(getIntent(Events))))
.
是的,我鼓励你坚持,过一段时间就会明白了。以上是关于在一个简单的 RxJS 示例中,如何在不使用主题或命令式操作的情况下管理状态?的主要内容,如果未能解决你的问题,请参考以下文章
在类中使用 RxJs 主题时 JSON.stringify 上的循环对象异常
如何在不需要 rxjs-compat 的情况下只导入 RxJS 6 中使用的运算符,如旧的 RxJS?