gpt4 book ai didi

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

转载 作者:可可西里 更新时间:2023-11-01 01:42:28 27 4
gpt4 key购买 nike

我已经用 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/

    27 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com