如何在简单的RxJS示例中不使用Subject或命令式操作来管理状态?

15

我已经开始尝试使用 RxJS 两周了,虽然我原则上喜欢它,但似乎找不到正确的状态管理模式并加以实现。所有文章和问题都似乎达成了以下共识:

  • 可能情况下应避免使用 Subject,而是通过变换来推送状态。
  • 完全弃用 .getValue()
  • 除了 DOM 操作之外,也许应避免使用 .do

所有这些建议的问题在于,除了“你会学习 Rx 的方式并停止使用 Subject”之外,没有任何文献直接说明你应该使用什么替代方案。

但我无法找到任何明确的示例,特别指出以无状态和函数式方式从多个其他流输入导致单个流/对象的添加和删除的正确方法。

在被指向相同方向之前,我想指出未涵盖的文献问题:

接下来是我第十次重写标准 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 或通过状态“肉球”来表示任何东西,并且在第一个答案中,我不确定这如何不会引入大量链接的流,这些流是孤立的/无限增长/必须按照精确顺序构建彼此。
1个回答

13

我认为你已经找到了一个很好的例子:http://jsbin.com/redeko/edit?js,output

你对这个实现的问题在于明确地使用了一个状态对象来添加和删除项目。

然而,这正是你正在寻找的良好实践。例如,如果你将该状态对象重命名为viewModel,那么可能会更加明显。

那么什么是状态?

可能还有其他的定义,但我喜欢按照以下方式来思考状态:

  • 给定一个不纯的函数f,即output = f(input),使得相同的输入可以有不同的输出,当它存在时,与该函数相关的状态是额外的变量,使得f(input) = output = g(input, state)成立,其中g是一个纯函数。

因此,如果函数的目的是将表示用户输入的对象匹配到一个待办事项数组中,并且如果我在已经有2个待办事项的待办事项列表上单击“添加”,则输出将是3个待办事项。如果我在只有一个待办事项的待办事项列表上做同样的事情(相同的输入),则输出将是2个待办事项。因此,相同的输入却有不同的输出。

这里允许转换该函数为一个纯函数的状态是待办事项数组的当前值。因此,我的输入变成了一个add点击,以及通过函数g传递的当前待办事项数组,该函数使用新的待办事项列表生成一个新的待办事项数组。这个g函数是纯函数。因此,通过在g中显式地表示其以前隐藏的状态,可以以无状态的方式实现f

这非常符合函数式编程的理念,即围绕组合纯函数。

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)。但我认为这两种实现都是无状态的。 - user3743222
理解这个概念可能需要一些时间,但是你的scan输入流是一系列函数(操作)的流,你可以将它们应用于当前状态以获取更新后的状态。 - user3743222
我也想提醒你,因为你似乎是一个新用户,可能不熟悉SO的规则,不要忘记给有用的答案点赞,并接受解决你问题的答案。 - user3743222
非常感谢您的快速回答,我刚刚看到了。虽然由于我的经验不足,我无法点赞,但是您的答案非常好,我已经标记为解决。我确实需要一些时间来完全理解这个问题,因为似乎我需要将每个CRUD操作合并到一个流中,以确保我的输入不会继续增长。但是我会重新吸收您的答案和我提到的原始教程,因为由于代码中一些更加CycleJS-centric的方面,我没有意识到那就是他在做的。谢谢! - brokenalarms
1
我鼓励你坚持下去,过一段时间就会变得清晰明了。 - user3743222
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接