草庐IT

javascript - 如何在一个简单的 RxJS 示例中不使用主题或命令式操作来管理状态?

coder 2024-05-09 原文

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

  • Subject在可能的情况下应该避免,以支持通过 via 转换来插入状态;
  • .getValue()应该完全弃用;和
  • .do除了 DOM 操作外,也许应该避免?

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

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

    在我再次指出相同的方向之前,未发现文献的问题是:
  • The Introduction to Reactive Programming 您一直缺少:很棒的起始文本,但没有专门解决这些问题。
  • RxJS 的 TODO 示例随 React 一起提供,涉及对 Subject 的显式操作。 s 作为 React Stores 的代理。
  • 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 问题中评论说它不应该真正公开(但同样,如果没有,那么它如何可用?)。

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

    最佳答案

    我想你已经找到了一个很好的例子: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 是纯函数。

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

    这里允许将该函数转换为纯函数的状态是 todo 数组的当前值。所以我的输入变成了 add点击,当前 todo 数组,通过函数 g它给出了一个新的 todo 数组和一个新的 todo 列表。那个函数 g 是纯函数。所以f通过在 g 中明确其先前隐藏的状态,以无状态方式实现.

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

    Rxjs 操作符
  • 扫描

  • 因此,当涉及到状态管理时,使用 RxJS 或其他方式,一个好的做法是使状态显式来操作它。

    如果你打开 output = g(input, state)进入一个流,你得到 On+1 = g(In+1, Sn)而这正是 scan运营商。
  • 展开

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

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

    关于javascript - 如何在一个简单的 RxJS 示例中不使用主题或命令式操作来管理状态?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36141280/

    有关javascript - 如何在一个简单的 RxJS 示例中不使用主题或命令式操作来管理状态?的更多相关文章

    1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

      我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

    2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

      总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

    3. ruby - i18n Assets 管理/翻译 UI - 2

      我正在使用i18n从头开始​​构建一个多语言网络应用程序,虽然我自己可以处理一大堆yml文件,但我说的语言(非常)有限,最终我想寻求外部帮助帮助。我想知道这里是否有人在使用UI插件/gem(与django上的django-rosetta不同)来处理多个翻译器,其中一些翻译器不愿意或无法处理存储库中的100多个文件,处理语言数据。谢谢&问候,安德拉斯(如果您已经在ruby​​onrails-talk上遇到了这个问题,我们深表歉意) 最佳答案 有一个rails3branchofthetolkgem在github上。您可以通过在Gemfi

    4. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

      关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

    5. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

      给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

    6. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

      我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

    7. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

      我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

    8. ruby - 如何指定 Rack 处理程序 - 2

      Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

    9. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

      使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

    10. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

      我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

    随机推荐