如何在简单的 RxJS 示例中不使用 Subject 或命令式操作来管理状态?
How to manage state without using Subject or imperative manipulation in a simple RxJS example?
我已经尝试使用 RxJS 两个星期了,虽然原则上我喜欢它,但我似乎无法找到并实现正确的状态管理模式。所有文章和问题似乎都一致:
应尽可能避免使用 Subject
,而只支持通过转换推动状态;
.getValue()
应该完全弃用;和
.do
也许应该避免,除了 DOM 操纵?
所有这些建议的问题在于 none 的文献似乎直接说明了你应该使用什么,除了 "you'll learn the Rx way and stop using Subject"。
但我无法在任何地方找到直接示例,具体说明以无状态和功能方式对单个 stream/object 执行添加和删除的正确方法,作为多个其他流输入的结果。
在我再次指向相同的方向之前,未发现文献的问题是:
- 响应式编程简介您错过了:很好的入门文本,但没有专门解决这些问题。
- RxJS 的 TODO 示例随 React 一起提供,涉及显式操作
Subject
s 作为 React Stores 的代理。
- 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 问题中评论说这是不应该真正公开的东西(但同样,如果不是,那么它如何使用?)。
我想我只是有一个想法,对于流,你不应该使用主题或通过状态表示任何东西 'meatball',在第一个答案中我不确定这是怎么回事' t 引入大量链式流,它们 orphaned/grow infinitely/have 以精确的顺序相互构建。
我认为您已经找到了一个很好的例子:http://jsbin.com/redeko/edit?js,output。
你对这个实现有异议
explicitly uses a state object for addition and removal of items.
但是,这正是您正在寻找的良好做法。例如,如果您重命名该状态对象 viewModel
,它对您来说可能更明显。
那么状态是什么?
会有其他定义,但我喜欢按如下方式考虑状态:
- 给定
f
一个不纯的函数,即 output = f(input)
,这样你可以对相同的输入有不同的输出,与该函数关联的状态(当它存在时)是额外的变量,例如f(input) = output = g(input, state)
成立并且 g 是一个纯函数。
因此,如果这里的函数是将表示用户输入的对象与待办事项数组相匹配,并且如果我在已经有 2 个待办事项的待办事项列表上单击 add
,则输出将为 3待办事项。如果我在只有一个待办事项的待办事项列表上执行相同的操作(相同的输入),输出将是 2 个待办事项。所以相同的输入,不同的输出。
此处允许将该函数转换为纯函数的状态是 todo 数组的当前值。所以我的输入变成了 add
单击,AND 当前的待办事项数组,通过一个函数 g
传递,它给出了一个新的待办事项数组和一个新的待办事项列表。该函数 g 是纯函数。因此 f
通过在 g
.
中显式显示其先前的隐藏状态,以无状态方式实现
这非常适合围绕编写纯函数的函数式编程。
Rxjs 运算符
- 扫描
因此,当涉及到状态管理时,使用 RxJS 或其他方式,一个好的做法是使状态显式地操作它。
如果将 output = g(input, state)
转换为流,则会得到 On+1 = g(In+1, Sn)
,这正是 scan
运算符所做的。
- 展开
另一个推广 scan
的运算符是 expand
,但到目前为止我很少使用该运算符。 scan
通常可以解决问题。
对于冗长而数学化的回答,我们深表歉意。我花了一些时间来理解这些概念,这就是我让它们对我来说易于理解的方式。希望它也适合你。
我已经尝试使用 RxJS 两个星期了,虽然原则上我喜欢它,但我似乎无法找到并实现正确的状态管理模式。所有文章和问题似乎都一致:
-
应尽可能避免使用
Subject
,而只支持通过转换推动状态;.getValue()
应该完全弃用;和.do
也许应该避免,除了 DOM 操纵?
所有这些建议的问题在于 none 的文献似乎直接说明了你应该使用什么,除了 "you'll learn the Rx way and stop using Subject"。
但我无法在任何地方找到直接示例,具体说明以无状态和功能方式对单个 stream/object 执行添加和删除的正确方法,作为多个其他流输入的结果。
在我再次指向相同的方向之前,未发现文献的问题是:
- 响应式编程简介您错过了:很好的入门文本,但没有专门解决这些问题。
- RxJS 的 TODO 示例随 React 一起提供,涉及显式操作
Subject
s 作为 React Stores 的代理。 - 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 问题中评论说这是不应该真正公开的东西(但同样,如果不是,那么它如何使用?)。
我想我只是有一个想法,对于流,你不应该使用主题或通过状态表示任何东西 'meatball',在第一个答案中我不确定这是怎么回事' t 引入大量链式流,它们 orphaned/grow infinitely/have 以精确的顺序相互构建。
我认为您已经找到了一个很好的例子:http://jsbin.com/redeko/edit?js,output。
你对这个实现有异议
explicitly uses a state object for addition and removal of items.
但是,这正是您正在寻找的良好做法。例如,如果您重命名该状态对象 viewModel
,它对您来说可能更明显。
那么状态是什么?
会有其他定义,但我喜欢按如下方式考虑状态:
- 给定
f
一个不纯的函数,即output = f(input)
,这样你可以对相同的输入有不同的输出,与该函数关联的状态(当它存在时)是额外的变量,例如f(input) = output = g(input, state)
成立并且 g 是一个纯函数。
因此,如果这里的函数是将表示用户输入的对象与待办事项数组相匹配,并且如果我在已经有 2 个待办事项的待办事项列表上单击 add
,则输出将为 3待办事项。如果我在只有一个待办事项的待办事项列表上执行相同的操作(相同的输入),输出将是 2 个待办事项。所以相同的输入,不同的输出。
此处允许将该函数转换为纯函数的状态是 todo 数组的当前值。所以我的输入变成了 add
单击,AND 当前的待办事项数组,通过一个函数 g
传递,它给出了一个新的待办事项数组和一个新的待办事项列表。该函数 g 是纯函数。因此 f
通过在 g
.
这非常适合围绕编写纯函数的函数式编程。
Rxjs 运算符
- 扫描
因此,当涉及到状态管理时,使用 RxJS 或其他方式,一个好的做法是使状态显式地操作它。
如果将 output = g(input, state)
转换为流,则会得到 On+1 = g(In+1, Sn)
,这正是 scan
运算符所做的。
- 展开
另一个推广 scan
的运算符是 expand
,但到目前为止我很少使用该运算符。 scan
通常可以解决问题。
对于冗长而数学化的回答,我们深表歉意。我花了一些时间来理解这些概念,这就是我让它们对我来说易于理解的方式。希望它也适合你。