如何在一个简单的 RxJS 示例中不使用主题或命令式操作来管理状态? [英] How to manage state without using Subject or imperative manipulation in a simple RxJS example?

查看:14
本文介绍了如何在一个简单的 RxJS 示例中不使用主题或命令式操作来管理状态?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I have been experimenting with RxJS for two weeks now, and although I love it in principle I just cannot seem to find and implement the correct pattern for managing state. All articles and questions appear to agree:

  • Subject 应该尽可能避免,以支持通过转换来推送状态;
  • .getValue() 应该完全弃用;和
  • .do 也许应该避免除了 DOM 操作?
  • Subject should be avoided where possible in favor of just pushing state through via transformations;
  • .getValue() should be deprecated entirely; and
  • .do should perhaps be avoided except for DOM manipulation?

所有这些建议的问题在于,除了您将学习 Rx 方式并停止使用 Subject"之外,似乎没有任何文献直接说明您应该使用什么.

The problem with all such suggestions is that none of the literature appears to directly say what you should be using instead, besides "you'll learn the Rx way and stop using Subject".

但我在任何地方都找不到一个直接的例子来明确指出以无状态和功能的方式对单个流/对象执行添加和删除的正确方法,这是多个其他流输入的结果.

But I cannot find a direct example anywhere that specifically indicates the correct way to perform both additions and removals to a single stream/object, as the consequence of multiple other stream inputs, in a stateless and functional manner.

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

Before I get pointed in the same directions again, problems with uncovered literature are:

  • The Introduction to Reactive Programming You've been missing: great starting text, but does not specifically address these questions.
  • The TODO example for RxJS comes with React and involves explicit manipulation of Subjects as proxies for React Stores.
  • http://blog.edanschwartz.com/2015/09/18/dead-simple-rxjs-todo-list/ : explicitly uses a state object for addition and removal of items.

我可能对标准 TODO 进行了第 10 次重写 - 我之前涵盖的迭代包括:

My perhaps 10th rewrite of the standard TODO follows - My prior iterations covered include:

  • 从可变的items"数组开始 - 因为状态是显式且强制管理的,所以很糟糕
  • 使用 scan 将新项目连接到 addedItems$ 流,然后分支另一个流,删除已删除的项目 - 与 addedItems$流将无限增长.
  • 发现 BehaviorSubject 并使用它 - 看起来很糟糕,因为对于每个新的 updatedList$.next() 发射,它需要迭代前一个值,这意味着 Subject.getValue() 是必不可少的.
  • 尝试将 inputEnter$ 添加事件的结果流式传输到过滤的删除事件中 - 但是每个新流都会创建一个新列表,然后将其输入 toggleItem$toggleAll$ 流意味着每个新流都依赖于前一个流,因此导致 4 个操作之一(添加、删除、切换项目或切换全部)需要不必要地运行整个链再次通过.
  • starting with a mutable 'items' array - bad as state is explicit and imperatively managed
  • using scan to concatenate new items to an addedItems$ stream, then branching another stream where the removed items were deleted - bad as the addedItems$ stream would grow indefinitely.
  • discovering BehaviorSubjectand using that - seemed bad since for each new updatedList$.next() emission, it requires the previous value to iterate, meaning that Subject.getValue() is essential.
  • trying to stream the result of the inputEnter$ addition events into filtered removal events - but then every new stream creates a new list, and then feeding that into the toggleItem$ and toggleAll$ streams means that each new stream is dependent on the previous, and so causing one of the 4 actions (add, remove, toggle item or toggle all) requires the whole chain to be unnecessarily run through again.

现在我又回到了原点,我又回到了同时使用 Subject(以及如何在不使用 getValue() 的情况下以任何方式连续迭代它)code>?) 和 do,如下所示.我自己和我的同事都同意这是最清晰的方式,但它当然似乎是最不被动和最必要的.任何关于正确方法的明确建议将不胜感激!

Now I have come full circle, where I am back to using both Subject (and just how is it supposed to be successively iterated upon in any way without using getValue()?) and do, as show below. Myself and my colleague agree this is the clearest way, yet it of course seems the least reactive and most imperative. Any clear suggestions on the correct way for this would be much appreciated!

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 是表示随时间演变的集合的最佳方式,之前状态的快照作为附加函数参数.

Edit

In relation to @user3743222 very helpful answer, I can see how representing state as an additional input can make a function pure and thus scan is the best way to represent a collection evolving over time, with a snapshot of its previous state up to that point as an additional function parameter.

然而,这已经是我第二次尝试的方式, addedItems$ 是输入的扫描流:

However, this was already how I approached my second attempt, with addedItems$ being a scanned stream of inputs:

// 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 问题中评论说这是不应该真正公开的东西(但同样,如果没有,那怎么用?).

The obvious solution would be to just have items = [], and manipulate it directly, or const items = new BehaviorSubject([]) - but then the only way to iterate on it appears to be using getValue to expose the previous state, which Andre Stalz (CycleJS) has commented on in the RxJS issues as something that shouldn't really be exposed (but again, if not, then how is it usable?).

我想我只是有一个想法,对于流,您不应该使用 Subjects 或通过状态肉丸"来表示任何内容,并且在第一个答案中,我不确定这如何不会引入质量链孤立的/无限增长/必须以精确顺序相互建立的流.

I guess I just had an idea that with streams, you weren't supposed to use Subjects or represent anything via a state 'meatball', and in the first answer I'm not sure how this doesn't introduce mass chained streams which are orphaned/grow infinitely/have to build on each other in exact sequence.

推荐答案

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

I think you already found a good example with : http://jsbin.com/redeko/edit?js,output.

你对这个实现有异议

明确使用状态对象来添加和删除项目.

explicitly uses a state object for addition and removal of items.

然而,这正是您正在寻找的良好做法.例如,如果您重命名该状态对象 viewModel,它可能对您更明显.

However, thas is exactly the good practice you are looking for. If you rename that state object viewModel for example, it might be more apparent to you.

还会有其他定义,但我喜欢将状态视为如下:

There will be other definitions but I like to think of state as follows:

  • 给定 f 一个不纯的函数,即 output = f(input),这样你就可以为同一个输入有不同的输出,即与该函数相关的状态(当它存在时) 是额外的变量,使得 f(input) = output = g(input, state) 成立并且 g 是一个纯函数.
  • given f an impure function, i.e. output = f(input), such that you can have different outputs for the same input, the state associated to that function (when it exists) is the extra variable such that f(input) = output = g(input, state) holds and g is a pure function.

因此,如果此处的函数是将表示用户输入的对象与待办事项数组匹配,并且如果我在已经有 2 个待办事项的待办事项列表上单击 add,则输出将是3 待办事项.如果我在只有一个待办事项的待办事项列表上执行相同(相同的输入),则输出将是 2 个待办事项.所以相同的输入,不同的输出.

So if the function here is to match an object representing a user input, to an array of todo, and if I click add on a todo list with already have 2 todos, the output will be 3 todos. If I do the same (same input) on a todo list with only one todo, the output will be 2 todos. So same input, different outputs.

此处允许将该函数转换为纯函数的状态是 todo 数组的当前值.所以我的输入变成了一个 add 点击,AND 当前的 todo 数组,通过一个函数 g 传递一个新的 todo 数组和一个新的 todo列表.那个函数 g 是纯函数.所以 f 是通过在 g 中显式显示其先前隐藏的状态以无状态方式实现的.

The state here that allows to transform that function into a pure function is the current value of the todo array. So my input becomes an add click, AND the current todo array, passed through a function g which give a new todo array with a new todo list. That function g is pure. So f is implemented in a stateless way by making its previously hidden state explicit in g.

这非常适合围绕组合纯函数的函数式编程.

And that fits well with functional programming which revolves around composing pure functions.

  • 扫描

所以当涉及到状态管理时,使用 RxJS 或其他方式,一个好的做法是让状态显式来操作它.

So when it comes to state management, with RxJS or else, a good practice is to make state explicit to manipulate it.

如果你把 output = g(input, state) 变成一个流,你会得到 On+1 = g(In+1, Sn) 而这正是scan 运算符的作用.

If you turn the output = g(input, state) into a stream, you get On+1 = g(In+1, Sn) and that's exactly what the scan operator does.

  • 展开

另一个概括 scan 的运算符是 expand,但到目前为止我很少使用该运算符.scan 通常可以解决问题.

Another operator which generalizes scan is expand, but so far I had very little use of that operator. scan generally does the trick.

很抱歉我的回答冗长而数学化.我花了一段时间来解决这些概念,这就是我让它们对我来说易于理解的方式.希望它也适用于您.

Sorry for the long and mathy answer. It took me a while to get around those concepts and that's the way I made them understandable for me. Hopefully it works for you too.

这篇关于如何在一个简单的 RxJS 示例中不使用主题或命令式操作来管理状态?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆