reactjs - 使用 reselect 计算派生状态时如何避免 React 重新渲染

标签 reactjs redux immutable.js reselect

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

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

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

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

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

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

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

在这种情况下,可能的方法是摆脱重新选择,并将逻辑移至 reducer 层次结构中,该层次结构将使用增量更新来维持所需的状态。

最佳答案

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

<小时/>

原始选择器很好地处理的挑战之一是最终信息是由以任意顺序交付的许多部分编译而成的。如果我决定在我的 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;

核心思想是这样的:

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

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

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

最后一点:如果不是单独处理那些轻量操作,这种方法本质上相当于用 deepEqual 替换 shallowEqual纯组件的 shouldComponentUpdate 方法内部。

关于reactjs - 使用 reselect 计算派生状态时如何避免 React 重新渲染,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36009013/

相关文章:

node.js - 具有 native react 的 Axios 不返回文档并使应用程序崩溃

reactjs - Antd Table 渲染内部属性和对象数组

reactjs - 使用React/Redux实现无限滚动和react-waypoint问题

javascript - 如何使用 Immutable 删除深层属性

javascript - 使用 immutable.js 操作 Redux 中嵌套数组中的数据

reactjs - mapStateToProps 必须返回一个对象。相反收到了Map{}?

javascript - className= {'container' } 与 className ='container' 有什么区别

javascript - react 路由器路径参数不起作用

javascript - 为什么我没有在 react 中获得更新的 Prop 值(value)

javascript - React Redux 确认导航