如何在不使用主题或命令式操作的情况下管理状态 在一个简单的 RxJS 示例中

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

本文关键字:一个 简单 RxJS 状态 情况下 操作 命令式 管理      更新时间:2023-09-26

我已经尝试了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通常可以解决问题。

对不起,冗长而数学的答案。我花了一段时间才绕过这些概念,这就是我让它们对我来说易于理解的方式。希望它也适合您。