7

我在我的应用程序中使用了 React + Redux + Reselect + Immutable.js 的终极组合。我喜欢重新选择的想法,因为它让我的状态(由减速器维护)尽可能简单。我使用选择器来计算我需要的实际状态,然后将其提供给 React 组件。

这里的问题是,reducer 中的一个小变化会导致选择器重新计算整个派生输出,结果整个 React UI 也会更新。我的纯组件不起作用。它很慢。

典型例子:我的数据的第一部分来自服务器,基本上是不可变的。第二部分由客户端维护,并使用 redux 操作进行变异。它们由单独的减速器维护。

我使用选择器将这两个部分合并为一个记录列表,然后将其传递给 React 组件。但很明显,当我更改其中一个对象中的单个内容时,会重新生成整个列表并创建 Records 的新实例。并且 UI 完全重新渲染。

显然,每次运行选择器并不是很有效,但仍然相当快,我愿意做出这种权衡(因为它确实使代码更简单、更干净)。问题是实际渲染速度很慢。

我需要做的是将新的选择器输出与旧的深度合并,因为 Immutable.js 库足够聪明,不会在没有任何更改时创建新实例。但由于选择器是无法访问先前输出的简单函数,我想这是不可能的。

我认为我目前的方法是错误的,我想听听其他想法。

在这种情况下,可能要走的路是摆脱重新选择,并将逻辑移动到减速器的层次结构中,这些减速器将使用增量更新来维护所需的状态。

4

3 回答 3

6

我解决了我的问题,但我想没有正确的答案,因为它确实取决于具体情况。就我而言,我决定采用这种方法:


原始选择器处理得很好的挑战之一是最终信息是从以任意顺序交付的许多部分编译而成的。如果我决定在我的 reducer 中逐步建立最终信息,我必须确保计算所有可能的场景(信息片段可能到达的所有可能顺序)并定义所有可能状态之间的转换。而通过重新选择,我可以简单地利用我目前拥有的东西并从中做出一些事情。

为了保留这个功能,我决定将选择器逻辑移动到包装父级 reducer中。

好吧,假设我有三个reducer,A、B和C,以及对应的选择器。每个处理一条信息。该片段可以从服务器加载,也可以来自客户端的用户。这将是我原来的选择器:

const makeFinalState(a, b, c) => (new List(a)).map(item => 
  new MyRecord({ ...item, ...(b[item.id] || {}), ...(c[item.id] || {}) });

export const finalSelector = createSelector(
  [selectorA, selectorB, selectorC],
  (a, b, c) => makeFinalState(a, b, c,));

(这不是实际的代码,但我希望它有意义。请注意,无论各个 reducer 的内容可用的顺序如何,选择器最终都会生成正确的输出。)

我希望我的问题现在很清楚。如果任何这些 reducer 的内容发生变化,选择器将从头开始重新计算,生成所有记录的全新实例,最终导致 React 组件的完全重新渲染。

我目前的解决方案看起来很简单:

export default function finalReducer(state = new Map(), action) {
  state = state
    .update('a', a => aReducer(a, action))
    .update('b', b => bReducer(b, action))
    .update('c', c => cReducer(c, action));

  switch (action.type) {
    case HEAVY_ACTION_AFFECTING_A:
    case HEAVY_ACTION_AFFECTING_B:
    case HEAVY_ACTION_AFFECTING_C:
      return state.update('final', final => (final || new List()).mergeDeep(
        makeFinalState(state.get('a'), state.get('b'), state.get('c')));

    case LIGHT_ACTION_AFFECTING_C:
      const update = makeSmallIncrementalUpdate(state, action.payload);
      return state.update('final', final => (final || new List()).mergeDeep(update))
  }
}

export const finalSelector = state => state.final;

核心思想是这样的:

  • 如果发生大事(即我从服务器获得大量数据),我会重建整个派生状态。
  • 如果发生了一些小事情(即用户选择了一个项目),我只是在原始减速器和包装父减速器中进行快速增量更改(存在一定的重复性,但必须同时实现一致性和良好的性能)。

与选择器版本的主要区别在于我总是将新状态与旧状态合并。Immutable.js 库足够聪明,不会用新的 Record 实例替换旧的 Record 实例,如果它们的内容完全相同的话。因此,原始实例被保留,因此相应的纯组件不会重新渲染。

显然,深度合并是一项代价高昂的操作,因此这不适用于非常大的数据集。但事实是,与 React 重新渲染和 DOM 操作相比,这种操作仍然很快。因此,这种方法可以在性能和代码可读性/简洁性之间做出很好的折衷。

最后说明:如果不是单独处理那些轻量级的动作,这种方法本质上相当于替换为纯组件shallowEqualdeepEqual内部shouldComponentUpdate方法。

于 2016-03-20T22:38:17.830 回答
1

这种场景通常可以通过重构UI 与状态的连接方式来解决。假设您有一个显示项目列表的组件:您可以将其连接到一个简单的 id 列表,并通过 id 将每个单独的项目连接到它的记录,而不是将其连接到已经构建的项目列表。这样,当记录发生更改时,id 列表本身不会更改,只会重新渲染相应的连接组件。

如果在您的情况下,如果记录是从该州的不同部分组装而成的,则产生单个记录的选择器本身可以连接到该特定记录的该州的相关部分。

现在,关于 immutable.js 与 reselect 的使用:如果您的状态的原始部分已经是 immutable.js 对象,这种组合效果最好。通过这种方式,您可以利用它们使用持久数据结构的事实,并且 reselect 的默认记忆功能效果最好。你总是可以重写这个记忆函数,但是感觉选择器应该访问它以前的返回值,如果经常表明它负责应该保持在状态中的数据/或者它一次收集太多数据,并且也许更细粒度的选择器会有所帮助。

于 2017-07-29T09:38:41.477 回答
0

看起来您描述的场景与我写re-reselect的原因非常接近。

re-reselect是一个小型reselect包装器,它使用记忆工厂动态初始化选择器。

(免责声明:我是 的作者re-reselect)。

于 2017-04-25T20:34:20.030 回答