我解决了我的问题,但我想没有正确的答案,因为它确实取决于具体情况。就我而言,我决定采用这种方法:
原始选择器处理得很好的挑战之一是最终信息是从以任意顺序交付的许多部分编译而成的。如果我决定在我的 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 操作相比,这种操作仍然很快。因此,这种方法可以在性能和代码可读性/简洁性之间做出很好的折衷。
最后说明:如果不是单独处理那些轻量级的动作,这种方法本质上相当于替换为纯组件shallowEqual
的deepEqual
内部shouldComponentUpdate
方法。