如何在走树结构时避免使用`shareReplay`

时间:2017-08-27 18:19:59

标签: javascript rxjs reactive-programming

我正在尝试使用RxJS重写包管理器(PM为pnpm,PR为here)。

在重写期间,我使用了很多.shareReplay(Infinity),我被告知这是不好的(我是反应式编程的初学者)

有人可以建议如何使用.shareReplay(Infinity)重写代码,如下所示:

'use strict'
const Rx = require('@reactivex/rxjs')

const nodes = [
  {id: 'a', children$: Rx.Observable.empty()},
  {id: 'b', children$: Rx.Observable.empty()},
  {id: 'c', children$: Rx.Observable.from(['a', 'b', 'd'])},
  {id: 'd', children$: Rx.Observable.empty()},
  {id: 'e', children$: Rx.Observable.empty()},
]

// I want this stream to be executed once, that is why the .shareReplay
const node$ = Rx.Observable.from(nodes).shareReplay(Infinity)

const children$ = node$.mergeMap(node => node.children$.mergeMap(childId => node$.single(node => node.id === childId)))

children$.subscribe(v => console.log(v))

1 个答案:

答案 0 :(得分:1)

groupBy运算符应该在这里工作。看看PR这可能是一个严重的过度简化,但这里是:

'use strict'
const Rx = require('@reactivex/rxjs')

const nodes = [
  {id: 'a', children$: Rx.Observable.empty()},
  {id: 'b', children$: Rx.Observable.empty()},
  {id: 'c', children$: Rx.Observable.from(['a', 'b', 'd'])},
  {id: 'd', children$: Rx.Observable.empty()},
  {id: 'e', children$: Rx.Observable.empty()},
]

Rx.Observable.from(nodes)
 // Group each of the nodes by its id
 .groupBy(node => node.id)
 // Flatten out each of the children by only forwarding children with the same id
 .flatMap(group$ => group$.single(childId => group$.key === childId))
 .subscribe(v => console.log(v));

<击>

编辑:比我想象的更难

好的,所以在我的第二次阅读中,我发现这需要比我最初想象的更多的工作,所以它不能简化如此简单。基本上,你将不得不在这里选择内存复杂性和时间复杂度,因为没有灵丹妙药。

从优化的角度来看,如果初始源只是一个数组,那么你可以删除shareReplay并且它将以完全相同的方式工作,因为当订阅和ArrayObservable时,唯一的开销将是迭代通过数组,重新运行源代码并没有任何额外的成本。

基本上我认为您可以考虑两个维度,即节点数m和平均子项数n。在速度优化版本中,您最终必须两次运行m,并且需要遍历“n”个节点。由于你有m * n个孩子,最糟糕的情况是所有孩子都是独一无二的。这意味着如果我没有记错,您需要执行(m + m*n)操作,这些操作会简化为O(m*n)。这种方法的缺点是您需要同时拥有Map(nodeId - &gt; Node)和Map以删除重复的依赖项。

'use strict'
const Rx = require('@reactivex/rxjs')

const nodes = [
  {id: 'a', children$: Rx.Observable.empty()},
  {id: 'b', children$: Rx.Observable.empty()},
  {id: 'c', children$: Rx.Observable.from(['a', 'b', 'd'])},
  {id: 'd', children$: Rx.Observable.empty()},
  {id: 'e', children$: Rx.Observable.empty()},
]

const node$ = Rx.Observable.from(nodes);

// Convert Nodes into a Map for faster lookup later
// Note this will increase your memory pressure.
const nodeMap$ = node$
  .reduce((map, node) => {
    map[node.id] = node;
    return map;
  });


node$
  // Flatten the children
  .flatMap(node => node.children$)
  // Emit only distinct children (you can remove this to relieve memory pressure
  // But you will still need to perform de-duping at some point.
  .distinct()
  // For each child find the associated node
  .withLatestFrom(nodeMap$, (childId, nodeMap) => nodeMap[childId])
  // Remove empty nodes, this could also be a throw if that is an error
  .filter(node => !!node)
  .subscribe(v => console.log(v));

另一种方法是使用与您类似的方法,其重点是以降低内存压力为代价。请注意,就像我说的那样,如果你的源是一个数组,你基本上可以删除shareReplay,因为它在重新计算时所做的就是重新迭代数组。这消除了额外Map的开销。虽然我认为你仍然需要明确删除重复。如果O(m^2*n)很小,则最坏情况的运行时复杂度为O(m^2)或简单n,因为您需要遍历所有子项,并且对于每个子项,您还需要迭代再次通过m找到匹配的节点。

const node$ = Rx.Observable.from(nodes);
node$
  // Flatten the children
  .flatMap(node => node.children$)
  // You may still need a distinct to do the de-duping
  .flatMap(childId => node$.single(n => n.id === childId)));

我会说第一种选择在几乎所有情况下都是可取的,但我会留给您确定您的用例。可能是你建立了一些在某些情况下选择一种算法而不是另一种算法的启发式算法。

旁注:对不起,这不是那么容易,但是爱pnpm所以要保持良好的工作!