Подтвердить что ты не робот

Как управлять состоянием без использования темы или императивной манипуляции в простом примере RxJS?

Я экспериментировал с RxJS уже две недели, и, хотя мне это нравится в принципе, я просто не могу найти и реализовать правильный шаблон для управления состоянием. Кажется, что все статьи и вопросы согласны:

  • Subject следует избегать, когда это возможно, в пользу простого нажатия состояния через преобразования;
  • .getValue() должен быть полностью исключен; и
  • .do следует избегать, за исключением DOM-манипуляции?

Проблема со всеми такими предложениями заключается в том, что ни одна из литературы, по-видимому, прямо не говорит, что вы должны использовать вместо этого, кроме "вы узнаете путь Rx и прекратите использование темы".

Но я не могу найти прямой пример в любом месте, который конкретно указывает правильный способ выполнять как добавления, так и удаления в один поток/объект, как следствие нескольких других потоков, без учета состояния и функционала.

Прежде чем я снова укажу в тех же направлениях, проблемы с непокрытой литературой:

  • Введение в реактивное программирование Вы отсутствовали: отличный стартовый текст, но конкретно не затрагивает эти вопросы.
  • Пример TODO для RxJS поставляется с React и включает явное манипулирование Subject в качестве прокси-серверов для хранилищ React.
  • http://blog.edanschwartz.com/2015/09/18/dead-simple-rxjs-todo-list/: явно использует объект state для добавления и удаления элементов.

Мой, возможно, 10-й пересмотр стандартного TODO - Мои предыдущие итерации включают:

  • начиная с массива mutable '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 для отображения предыдущего состояния, которое Андре Стальц (CycleJS) прокомментировал в проблемах RxJS что-то, что не должно быть действительно разоблачено (но опять же, если нет, то как это можно использовать?).

Я предполагаю, что у меня просто появилась идея, что с потоками вы не должны использовать Субъекты или представлять что-либо через состояние "фрикадельки", и в первом ответе я не уверен, как это не приводит к массовым цепям потоки, которые осиротеть/расти бесконечно/должны строить друг на друга в точном порядке.

4b9b3361

Ответ 1

Я думаю, что вы уже нашли хороший пример: http://jsbin.com/redeko/edit?js,output.

Возникает проблема с тем, что эта реализация

явно использует объект состояния для добавления и удаления элементов.

Однако это именно то, что вы ищете. Если вы переименуете этот объект состояния viewModel, например, это может быть более очевидно для вас.

Итак, что такое состояние?

Будут другие определения, но мне нравится думать о состоянии следующим образом:

  • задана f нечистая функция, т.е. output = f(input), так что вы можете иметь разные выходы для одного и того же входа, состояние, связанное с этой функцией (когда оно существует), является дополнительной переменной, так что f(input) = output = g(input, state) выполняется и g - чистая функция.

Итак, если функция здесь соответствует объекту, представляющему пользовательский ввод, массиву todo, и если я нажму add в списке задач, у которого уже есть 2 todos, вывод будет 3 todos. Если я сделаю то же самое (тот же ввод) в списке задач с одним только одним, выход будет 2 todos. Такой же вход, разные выходы.

Состояние здесь, которое позволяет преобразовать эту функцию в чистую функцию, является текущим значением массива 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 обычно делает трюк.

Извините за долгий и матовый ответ. Мне потребовалось некоторое время, чтобы обойти эти концепции и понять, как я сделал их понятными для меня. Надеюсь, это сработает и для вас.