NGRX 选择器更新过于频繁

NGRX selectors updating too often

我真的很困惑,我真的以为如果他的所有 parents 返回相同的结果,选择器就不会 运行。

同时有 1-250 个 clusterMarker 选择器处于活动状态,每个选择器都有不同的道具,cluster。它的执行相当昂贵。我确定,仅当其任何参数的结果发生变化时才需要重新评估它。

简化示例:

const offerState = createFeatureSelector<OfferState>('offer');
const currentCarrier = createSelector(offerState, state => state.currentCarrier);
const currentContext = createSelector(offerState, state => state.currentContext);
const currentPeriod = createSelector(offerState, state => state.currentPeriod);
const filteredCarriers = createSelector(offerState, state => state.filteredCarriers);
const wanted = createSelector(offerState, state => state.wanted);

const filteredCarriersSet = createSelector(
    filteredCarriers,
    carriers => new Set(carriers),
);

/**
 * Only fire if the changes to state affect this context.
 * `undefined` => `state.currentCarrier`
 * `state.currentCarrier` => `undefined`
 */
const currentCarrierInCluster = createSelector(
    currentCarrier,
    currentContext,
    (
        currentCarrier: Carrier | null,
        currentContext: AbstractMapClusterContext<Carrier> | null,
        { cluster }: { cluster: AbstractMapClusterContext<Carrier> }
    ) => currentContext == cluster ? currentCarrier : undefined,
);

export const clusterMarker = createSelector(
    filteredCarriersSet,
    currentCarrierInCluster,
    currentPeriod,
    wanted,
    (
        filteredSet,
        currentCarrier,
        currentPeriod,
        wanted,
        { cluster }: { cluster: AbstractMapClusterContext<Carrier> }
    ) => {
        // ... code ...
    },
);

我是否错过了有关设置记忆选项的部分文档?我该怎么做才能提高性能?

回复回答:

代码:

export const clusterMarkerSelectorFactory = () =>  createSelector(
    filteredCarriersSet,
    currentCarrierInCluster,
    currentPeriod,
    wanted,
    (
        filteredSet,
        currentCarrier,
        currentPeriod,
        wanted,
        { cluster }: { cluster: AbstractMapClusterContext<Carrier> }
    ) => {
        // ... code ...
    },
);

class Component {
    constructor(
        private store$: Store<OfferState>,
    ) { }

    readonly state$ = this.cluster$.pipe(
        switchMap(cluster => this.store$.select(clusterMarkerSelectorFactory(), { cluster })),
    );
}

这仍然会为它们中的每一个重新触发。

所有 输入相同时,Memoization 仅记住单个 最后计算的值。

在您的示例中,currentCarrierInCluster 是使用不同的 cluster 参数调用的(尽管从您的语法中不清楚如何调用)。这使得记忆无效。

Memoization 的工作原理是将所有输入与其上次调用选择器时的等效值进行比较。如果它们相同,则缓存值被 returned,如果它们不同,则通过选择器的投影函数计算新值。

在您的情况下,cluster 输入不同,因此投影函数再次运行。

您可以通过将选择器包装在工厂函数中来解决这个问题,从而为每个 cluster 输入值创建不同的选择器实例:

const currentCarrierInCluster = (cluster: ClusterType) => createSelector(
    currentCarrier,
    currentContext,
    (
        currentCarrier: Carrier | null,
        currentContext: AbstractMapClusterContext<Carrier> | null,
        { cluster }: { cluster: AbstractMapClusterContext<Carrier> }
    ) => currentContext == cluster ? currentCarrier : undefined,
);

可以这样调用:

cluster$ = this.store.select(currentCarrierInCluster(cluster));

参考资料
https://ngrx.io/guide/store/selectors

更多实施信息

NgRx 选择器源代码是 here

请参阅第 102 行对 isArgumentsChanged 的调用,该调用确定输入值是否已更改,因此是否 return 记忆结果。它有效地对每个参数调用以下方法:

export function isEqualCheck(a: any, b: any): boolean {
  return a === b;
}

这可以被覆盖,但只有在特殊情况下才会这样做,

您的选择器需要更改

// Note = after clusterMarker rather than after =>
export const clusterMarker = (_cluster: AbstractMapClusterContext<Carrier>) => createSelector(
    filteredCarriersSet,
    currentCarrierInCluster,
    currentPeriod,
    wanted,
    (
        filteredSet,
        currentCarrier,
        currentPeriod,
        wanted,
        /* Remove this and use _cluster instead
       { cluster }: { cluster: AbstractMapClusterContext<Carrier> } */
    ) => {
        // ... code ... 
    },
);

像这样打电话:

this.store$.select(clusterMarker(cluster))

我现在知道为什么会这样了,我也有几个解决方法。这一切都与这样一个事实有关,即每当使用参数调用选择器时,其所有父级也将使用 参数 进行调用!我在上面创建了一个 GitHub issue

tldr;

原文:

// replace
const elementSelectorFactory = () => createSelector(
    listAsSet,
    (set, { element }) => set.has(element),
);

固定 v1:

// with (This needs to be done only for the first level of selectors, more info bellow)
const elementSelectorFactory = () => createSelector(
    state => listAsSet(state as any),
    (set, { element }) => set.has(element),
);

固定 v2:

// or with
const elementSelectorFactory = element => createSelector(
    listAsSet,
    set => set.has(element),
);

// or in case you have nested props selectors (my case)
const elementSelectorFactory = element => createSelector(
    listAsSet,
    nestedSelectorFactory(element),
    (set, nested) => set.has(element) ? nested : null,
);

我将带您了解一个带有 props 的简单选择器。

我们有下面的代码

interface Store {
  elements: unknown[];
}

export const offerState = createFeatureSelector<Store>('store');

const listAsSet = createSelector(
    state => state.list,
    carriers => new Set(carriers),
);

// We create a factory like they instructed us in the docs
const elementSelectorFactory = () => createSelector(
    listAsSet,
    (set, { element }) => set.has(element),
);

现在如果我们调用 this.store$.select(elementSelectorFactory(), { element }).

  1. 我们创建 elementSelector
  2. 此选择器将使用以下参数从其父级请求数据:[state, { element }]
  3. listAsSet 使用 stateprops { element } 调用,即使它不使用 props。

现在,如果您要从工厂创建 2 个 elementSelector,步骤 3 将被调用两次,使用 2 个不同的道具!由于其参数不相等,因此必须重新计算。相同元素的 Set() 不相等,因此这将使我们的 elementSelector!

的参数无效

更糟糕的是,如果您在其他地方使用 listAsSet 选择器而不使用 props,它也会失效!

这是我的第一个解决方案:

const elementSelectorFactory = () => createSelector(
    // Here we don't pass the props to the child selector, to prevent invalidating its cache
    (state, _props) => listAsSet(state as any),
    (set, { element }) => set.has(element),
);

下一个解决方案是完全不使用 props,正如 ngrx 成员所建议的...

// or with
const elementSelectorFactory = element => createSelector(
    listAsSet,
    set => set.has(element),
);

在这里,选择器将永远不会收到道具,首先......如果你想要嵌套选择器,你将不得不将它们全部重写为带有参数的选择器工厂来解决这个问题。

const elementSelectorFactory = element => createSelector(
    listAsSet,
    nestedSelectorFactory(element),
    (set, nested) => set.has(element) ? nested : null,
);