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

How to manage state without using Subject or imperative manipulation in a simple RxJS example?

我已经尝试使用 RxJS 两个星期了,虽然原则上我喜欢它,但我似乎无法找到并实现正确的状态管理模式。所有文章和问题似乎都一致:

所有这些建议的问题在于 none 的文献似乎直接说明了你应该使用什么,除了 "you'll learn the Rx way and stop using Subject"。

但我无法在任何地方找到直接示例,具体说明以无状态和功能方式对单个 stream/object 执行添加和删除的正确方法,作为多个其他流输入的结果。

在我再次指向相同的方向之前,未发现文献的问题是:

我对标准 TODO 的第 10 次重写如下 - 我之前的迭代包括:

现在我又绕了一圈,我又回到了使用 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 通常可以解决问题。

对于冗长而数学化的回答,我们深表歉意。我花了一些时间来理解这些概念,这就是我让它们对我来说易于理解的方式。希望它也适合你。