使用 Kefir 用多个事件改变 属性 的惯用方法

Idiomatic way to mutate a property with multiple events using Kefir

在 Kefir 中创建响应多种事件类型而改变的 属性 的惯用方法是什么?

在我的项目中,我开始使用 rxjs 作为 FRP 样式的应用程序。在此应用程序中,我想订阅响应 多个 变量而更改的状态。这就是我让它工作的方式:

const subject = new BehaviorSubject([]);

addEvents
  .withLatestFrom(subject, (newItem, items) => items.concat(newItem))
  .subscribe(subject);

removeEvents
  .withLatestFrom(subject, (item, items) => {
    return items.filter(i => i !== item);
  })
  .subscribe(subject);

我看得出来这可能不是最佳做法;这似乎不是惯用的,而且我也刚刚发现为观察者订阅多个来源并不正确。

出于多种原因,我决定尝试使用与 RxJs 不同的库,并且现在正在评估 Kefir,它具有出色的文档和据说更好的性能。但我发现要确定如何做我想做的事情更加困难,除了我必须检查事件类型的丑陋黑客:

kefir
  .merge(addEvents, removeEvents)
  .scan(
    (items, event) => {
      switch (event.type) {
        case 'add': return items.concat(event.item);
        case 'remove': return items.filter(i => i !== event.item);
      }
    },
    [])
  .toProperty();

我真的更喜欢 而不是 必须使用一些不优雅的技术,例如大量事件类型的大条件块,以便创建变化流。

我不打算使用 Bacon.js,但我发现它正是我需要的:

Bacon.update([],
  [addEvents],    (items, evt) => items.concat(evt.item),
  [removeEvents], (items, evt) => items.filter(i => i !== evt.item));

是否有一种自然的方法可以使用 Kefir 的标准运算符来完成此类操作,或者这是我最终不得不自己实现的方法吗?

我想出了这种方法,但我并不喜欢这种方法,但至少代码非常干净:

const items = function () {
  let items = [];  
  return kefir
    .merge([
      addEvents.map(item => items = items.concat(item)),
      removeEvents.map(item => items = items.filter(i => i !== item))
    ])
    .toProperty(() => items);
}();

我不喜欢的是它会改变状态,但由于 JavaScript 是单线程的并且我隐藏了那个状态,也许它还不错。

这是一个效用函数:

import kefir from 'kefir';

export default function dynamicValue(initValue, ...args) {
  let value = initValue;
  let streams = [];
  while (args.length) {
    let [source, xform, ...remaining] = args;
    streams.push(source.map(v => value = xform(value, v)));
    args = remaining;
  }

  return kefir.merge(streams).toProperty(() => value);
}

...这样使用:

dynamicValue([],
  addEvents, (items, item) => items.concat(item),
  removeEvents, (items, item) => items.filter(i => i !== item));

更新:

想出一种不同的方法来使用 mapmergescan 来实现 dynamicValue,而无需改变变量:

import kefir from 'kefir';

export default function transform(initValue, ...args) {
  let mutations = [];
  while (args.length) {
    let [source, calculateNewValue, ...newArgs] = args;
    mutations.push(source.map(e => ({ event: e, mutation: calculateNewValue })));
    args = newArgs;
  }

  return kefir
    .merge(mutations)
    .scan((prev, { event, mutation }) => mutation(prev, event), initValue);
}

基本上,它将每个事件与突变函数配对,合并这些对,然后扫描它们,将突变函数应用于原始值和事件。我不确定它是否真的更好,但它确实看起来更 "functional."