在一个简单的 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 一起提供,涉及将 Subjects 作为 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?

如何在 Angular 12 中制作一个简单的 rxjs/webSocket 服务?

如何在 RxJS 订阅方法中等待

RxJS:如何切换以在多个行为主题源之间切换

RxJS主题(Subject)