使用重新选择计算派生状态时如何避免 React 重新渲染
How to avoid React re-renders when using reselect to compute derived state
我在我的应用程序中使用了 React + Redux + Reselect + Immutable.js 的终极组合。我喜欢重新选择的想法,因为它让我的状态(由 reducer 维护)尽可能简单。我使用选择器来计算我需要的实际状态,然后将其提供给 React 组件。
这里的问题是,reducer 中的一个小变化会导致选择器重新计算整个导出的输出,结果整个 React UI 也会更新。 我的纯组件不起作用。很慢。
典型例子:我的第一部分数据来自服务器,基本上是不可变的。第二部分由客户端维护,并使用 redux 操作进行修改。它们由单独的减速器维护。
我使用选择器将两个部分合并为一个记录列表,然后将其传递给 React 组件。但显然,当我更改其中一个对象中的单个内容时,将重新生成整个列表并创建 Records 的新实例。 UI 完全重新渲染。
显然 运行 选择器每次都不是非常有效,但仍然相当快,我愿意做出这种权衡(因为它确实使代码更简单、更清晰)。问题是实际渲染很慢。
我需要做的是将新选择器输出与旧选择器输出深度合并,因为 Immutable.js 库足够聪明,不会在没有任何更改时创建新实例。但是由于选择器是无法访问先前输出的简单函数,我想这是不可能的。
我认为我目前的做法是错误的,我想听听其他想法。
在这种情况下,可能要走的路是摆脱重新选择,并将逻辑移动到使用增量更新来维护所需状态的缩减器层次结构中。
我解决了我的问题,但我想没有正确答案,因为这真的取决于具体情况。就我而言,我决定采用这种方法:
原始选择器很好地处理的挑战之一是最终信息是从以任意顺序交付的许多片段中编译而来的。如果我决定在我的 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 实例记录实例。因此保留了原始实例,因此不会重新渲染相应的纯组件。
显然,深度合并是一项代价高昂的操作,因此这不适用于非常大的数据集。但事实是,与 React 重新渲染和 DOM 操作相比,这种操作仍然很快。所以这种方法可以很好地折衷性能和代码 readability/conciseness.
最后说明:如果不是那些轻动作单独处理,这种方法本质上等同于将shallowEqual
替换为deepEqual
shouldComponentUpdate
纯组分法
看来您描述的场景与我写 re-reselect 的原因非常接近。
re-reselect
是一个小型 reselect
包装器,它使用记忆工厂动态初始化选择器。
(免责声明:我是 re-reselect
的作者)。
这种情况通常可以通过重构如何 UI 连接到状态来解决。假设您有一个显示项目列表的组件:您可以将它连接到一个简单的 id 列表,而不是将它连接到已经构建的项目列表,并通过 id 将每个单独的项目连接到它的记录。这样,当记录发生变化时,id 列表本身不会发生变化,只会重新呈现相应的连接组件。
如果在您的情况下,如果记录由州的不同部分组成,则生成单个记录的选择器本身可以连接到该特定记录的州的相关部分。
现在,关于 immutable.js 与重新选择的使用:如果您的状态的原始部分已经是 immutable.js 对象,则此组合最有效。通过这种方式,您可以利用它们使用持久数据结构这一事实,并且 reselect 的默认记忆功能效果最好。你总是可以重写这个记忆函数,但是感觉一个选择器应该访问它以前的 return 值,如果这通常是一个迹象表明它负责应该保存在状态中的数据/或者它正在收集太多数据一次,也许更细化的选择器会有所帮助。
我在我的应用程序中使用了 React + Redux + Reselect + Immutable.js 的终极组合。我喜欢重新选择的想法,因为它让我的状态(由 reducer 维护)尽可能简单。我使用选择器来计算我需要的实际状态,然后将其提供给 React 组件。
这里的问题是,reducer 中的一个小变化会导致选择器重新计算整个导出的输出,结果整个 React UI 也会更新。 我的纯组件不起作用。很慢。
典型例子:我的第一部分数据来自服务器,基本上是不可变的。第二部分由客户端维护,并使用 redux 操作进行修改。它们由单独的减速器维护。
我使用选择器将两个部分合并为一个记录列表,然后将其传递给 React 组件。但显然,当我更改其中一个对象中的单个内容时,将重新生成整个列表并创建 Records 的新实例。 UI 完全重新渲染。
显然 运行 选择器每次都不是非常有效,但仍然相当快,我愿意做出这种权衡(因为它确实使代码更简单、更清晰)。问题是实际渲染很慢。
我需要做的是将新选择器输出与旧选择器输出深度合并,因为 Immutable.js 库足够聪明,不会在没有任何更改时创建新实例。但是由于选择器是无法访问先前输出的简单函数,我想这是不可能的。
我认为我目前的做法是错误的,我想听听其他想法。
在这种情况下,可能要走的路是摆脱重新选择,并将逻辑移动到使用增量更新来维护所需状态的缩减器层次结构中。
我解决了我的问题,但我想没有正确答案,因为这真的取决于具体情况。就我而言,我决定采用这种方法:
原始选择器很好地处理的挑战之一是最终信息是从以任意顺序交付的许多片段中编译而来的。如果我决定在我的 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 实例记录实例。因此保留了原始实例,因此不会重新渲染相应的纯组件。
显然,深度合并是一项代价高昂的操作,因此这不适用于非常大的数据集。但事实是,与 React 重新渲染和 DOM 操作相比,这种操作仍然很快。所以这种方法可以很好地折衷性能和代码 readability/conciseness.
最后说明:如果不是那些轻动作单独处理,这种方法本质上等同于将shallowEqual
替换为deepEqual
shouldComponentUpdate
纯组分法
看来您描述的场景与我写 re-reselect 的原因非常接近。
re-reselect
是一个小型 reselect
包装器,它使用记忆工厂动态初始化选择器。
(免责声明:我是 re-reselect
的作者)。
这种情况通常可以通过重构如何 UI 连接到状态来解决。假设您有一个显示项目列表的组件:您可以将它连接到一个简单的 id 列表,而不是将它连接到已经构建的项目列表,并通过 id 将每个单独的项目连接到它的记录。这样,当记录发生变化时,id 列表本身不会发生变化,只会重新呈现相应的连接组件。
如果在您的情况下,如果记录由州的不同部分组成,则生成单个记录的选择器本身可以连接到该特定记录的州的相关部分。
现在,关于 immutable.js 与重新选择的使用:如果您的状态的原始部分已经是 immutable.js 对象,则此组合最有效。通过这种方式,您可以利用它们使用持久数据结构这一事实,并且 reselect 的默认记忆功能效果最好。你总是可以重写这个记忆函数,但是感觉一个选择器应该访问它以前的 return 值,如果这通常是一个迹象表明它负责应该保存在状态中的数据/或者它正在收集太多数据一次,也许更细化的选择器会有所帮助。