重新选择 - 调用另一个选择器的选择器?

IT技术 javascript reactjs reselect
2021-04-28 13:22:04

我有一个选择器:

const someSelector = createSelector(
   getUserIdsSelector,
   (ids) => ids.map((id) => yetAnotherSelector(store, id),
);                                      //     ^^^^^ (yetAnotherSelector expects 2 args)

yetAnotherSelector是另一个选择器,它接受用户 ID -id并返回一些数据。

但是,由于它是createSelector,因此我无权存储在其中(我不希望将其作为函数,因为那时无法进行记忆)。

有没有办法以某种方式访问​​商店内部createSelector或者有没有其他的办法来处理?

编辑

我有一个功能:

const someFunc = (store, id) => {
    const data = userSelector(store, id);
              // ^^^^^^^^^^^^ global selector
    return data.map((user) => extendUserDataSelector(store, user));
                       //     ^^^^^^^^^^^^^^^^^^^^ selector
}

这样的功能正在扼杀我的应用程序,导致所有内容重新渲染并使我发疯。帮助表示赞赏。

!! 然而:

我做了一些基本的自定义记忆:

import { isEqual } from 'lodash';

const memoizer = {};
const someFunc = (store, id) => {
    const data = userSelector(store, id);
    if (id in memoizer && isEqual(data, memoizer(id)) {
       return memoizer[id];
    }

    memoizer[id] = data;
    return memoizer[id].map((user) => extendUserDataSelector(store, user));
}

而且的伎俩,不过,是不是只是一个解决方法吗?

4个回答

为您的 someFunc 案例

对于您的特定情况,我将创建一个本身返回扩展器的选择器。

也就是说,为此:

const someFunc = (store, id) => {
    const data = userSelector(store, id);
              // ^^^^^^^^^^^^ global selector
    return data.map((user) => extendUserDataSelector(store, user));
                       //     ^^^^^^^^^^^^^^^^^^^^ selector
}

我会写:

const extendUserDataSelectorSelector = createSelector(
  selectStuffThatExtendUserDataSelectorNeeds,
  (state) => state.something.else.it.needs,
  (stuff, somethingElse) =>
    // This function will be cached as long as
    // the results of the above two selectors
    // does not change, same as with any other cached value.
    (user) => {
      // your magic goes here.
      return {
        // ... user with stuff and somethingElse
      };
    }
);

然后someFunc会变成:

const someFunc = createSelector(
  userSelector,
  extendUserDataSelectorSelector,
  // I prefix injected functions with a $.
  // It's not really necessary.
  (data, $extendUserDataSelector) =>
    data.map($extendUserDataSelector)
);

我将其称为 reifier 模式,因为它创建了一个预先绑定到当前状态并接受单个输入并将其具体化的函数。我通常将它用于通过 id 获取事物,因此使用“reify”。我也喜欢说“reify”,老实说,这是我这么称呼它的主要原因。

对于您的情况

在这种情况下:

import { isEqual } from 'lodash';

const memoizer = {};
const someFunc = (store, id) => {
    const data = userSelector(store, id);
    if (id in memoizer && isEqual(data, memoizer(id)) {
       return memoizer[id];
    }

    memoizer[id] = data;
    return memoizer[id].map((user) => extendUserDataSelector(store, user));
}

这基本上就是重新选择所做的。如果您计划在全局级别实施 per-id memoization,您可能希望考虑这一点。

import createCachedSelector from 're-reselect';

const someFunc = createCachedSelector(
  userSelector,
  extendUserDataSelectorSelector,
  (data, $extendUserDataSelector) =>
    data.map($extendUserDataSelector)
// NOTE THIS PART DOWN HERE!
// This is how re-reselect gets the cache key.
)((state, id) => id);

或者,您可以将您的 memoized-multi-selector-creator 用蝴蝶结包裹起来并调用它createCachedSelector,因为它基本上是相同的。

编辑:为什么要返回函数

另一种方法是只选择运行extendUserDataSelector计算所需的所有适当数据,但这意味着将所有其他想要使用该计算的函数暴露在其接口中。通过返回一个只接受一个user基础数据的函数,你可以保持其他选择器的接口干净。

编辑:关于收藏

上述实现目前易受攻击的一件事是 ifextendUserDataSelectorSelector的输出更改,因为它自己的依赖项选择器发生了变化,但userSelector通过extendUserDataSelectorSelector. 在这些情况下,您需要做两件事:

  1. 多记忆extendUserDataSelectorSelector返回的函数我建议将其提取到一个单独的全局记忆函数中。
  2. 换行,someFunc以便当它返回一个数组时,它将该数组元素与前一个结果进行比较,如果它们具有相同的元素,则返回前一个结果。

编辑:避免如此多的缓存

如上所示,在全局级别缓存当然是可行的,但是如果您考虑到其他几个策略来解决问题,则可以避免这种情况:

  1. 不要急切地扩展数据,将其推迟到实际呈现数据本身的每个 React(或其他视图)组件。
  2. 不要急切地将 ids/base-objects 列表转换为扩展版本,而是让父母将这些 ids/base-objects 传递给孩子们。

在我的一个主要工作项目中,我一开始没有遵循这些,但我希望我有。事实上,我后来不得不转而采用全局记忆路线,因为这比重构所有视图更容易修复,这是应该完成但我们目前缺乏时间/预算的事情。

编辑 2(或 4 我猜?):重新考虑收藏 pt。1:多重记忆扩展器

注意:在您阅读这部分之前,它假定传递给扩展器的基础实体将具有某种id可用于唯一标识它属性,或者可以廉价地从中派生出某种类似的属性。

为此,您可以以类似于任何其他选择器的方式记住扩展器本身。但是,由于您希望 Extender 记住其参数,因此您不想将 State 直接传递给它。

基本上,您需要一个 Multi-Memoizer,它的作用基本上与Re-reselect对 Selectors 的作用相同事实上,createCachedSelector为我们做这件事是微不足道的

function cachedMultiMemoizeN(n, cacheKeyFn, fn) {
  return createCachedSelector(
    // NOTE: same as [...new Array(n)].map((e, i) => Lodash.nthArg(i))
    [...new Array(n)].map((e, i) => (...args) => args[i]),
    fn
  )(cacheKeyFn);
}

function cachedMultiMemoize(cacheKeyFn, fn) {
  return cachedMultiMemoizeN(fn.length, cacheKeyFn, fn);
}

然后代替旧的extendUserDataSelectorSelector

const extendUserDataSelectorSelector = createSelector(
  selectStuffThatExtendUserDataSelectorNeeds,
  (state) => state.something.else.it.needs,
  (stuff, somethingElse) =>
    // This function will be cached as long as
    // the results of the above two selectors
    // does not change, same as with any other cached value.
    (user) => {
      // your magic goes here.
      return {
        // ... user with stuff and somethingElse
      };
    }
);

我们有这两个功能:

// This is the main caching workhorse,
// creating a memoizer per `user.id`
const extendUserData = cachedMultiMemoize(
  // Or however else you get globally unique user id.
  (user) => user.id,
  function $extendUserData(user, stuff, somethingElse) {
    // your magic goes here.
    return {
      // ...user with stuff and somethingElse
    };
  }
);

// This is still wrapped in createSelector mostly as a convenience.
// It doesn't actually help much with caching.
const extendUserDataSelectorSelector = createSelector(
  selectStuffThatExtendUserDataSelectorNeeds,
  (state) => state.something.else.it.needs,
  (stuff, somethingElse) =>
    // This function will be cached as long as
    // the results of the above two selectors
    // does not change, same as with any other cached value.
    (user) => extendUserData(
      user,
      stuff,
      somethingElse
    )
);

extendUserData就是真正的缓存发生的地方,尽管有公平的警告:如果你有很多baseUser实体,它可能会变得非常大。

编辑 2(或 4 我猜?):重新考虑收藏 pt。2:数组

数组是缓存存在的祸根:

  1. arrayOfSomeIds 本身可能不会改变,但其中的 id 指向的实体可能会改变。
  2. arrayOfSomeIds 可能是内存中的一个新对象,但实际上具有相同的 ID。
  3. arrayOfSomeIds 没有改变,但是包含被引用实体的集合确实改变了,但是这些特定 id 引用的特定实体没有改变。

这就是为什么我提倡将数组(和其他集合!)的扩展/扩展/具体化/任何其他化的任务委派到数据获取-派生-视图-渲染过程中尽可能晚的时间:杏仁核的痛苦是考虑这一切。

也就是说,这并非不可能,只是需要进行一些额外的检查。

从上面的缓存版本开始someFunc

const someFunc = createCachedSelector(
  userSelector,
  extendUserDataSelectorSelector,
  (data, $extendUserDataSelector) =>
    data.map($extendUserDataSelector)
// NOTE THIS PART DOWN HERE!
// This is how re-reselect gets the cache key.
)((state, id) => id);

然后我们可以将它包装在另一个只缓存输出的函数中:

function keepLastIfEqualBy(isEqual) {
  return function $keepLastIfEqualBy(fn) {
    let lastValue;

    return function $$keepLastIfEqualBy(...args) {
      const nextValue = fn(...args);
      if (! isEqual(lastValue, nextValue)) {
        lastValue = nextValue;
      }
      return lastValue;
    };
  };
}

function isShallowArrayEqual(a, b) {
  if (a === b) return true;
  if (Array.isArray(a) && Array.isArray(b)) {
    if (a.length !== b.length) return false;
    // NOTE: calling .every on an empty array always returns true.
    return a.every((e, i) => e === b[i]);
  }
  return false;
}

现在,我们不能仅将其应用于 的结果createCachedSelector,这仅适用于一组输出。相反,我们需要为每个createCachedSelector创建的底层选择器使用它幸运的是,重新选择允许您配置它使用的选择器创建者:

const someFunc = createCachedSelector(
  userSelector,
  extendUserDataSelectorSelector,
  (data, $extendUserDataSelector) =>
    data.map($extendUserDataSelector)
)((state, id) => id,
  // NOTE: Second arg to re-reselect: options object.
  {
    // Wrap each selector that createCachedSelector itself creates.
    selectorCreator: (...args) =>
      keepLastIfEqualBy(isShallowArrayEqual)(createSelector(...args)),
  }
)

奖励部分:数组输入

您可能已经注意到,我们只检查数组输出,涵盖情况 1 和 3,这可能已经足够了。但是,有时您可能还需要捕获案例 2,检查输入数组。这可以通过使用 reselectcreateSelectorCreator使用自定义相等函数制作我们自己的createSelector

import { createSelectorCreator, defaultMemoize } from 'reselect';

const createShallowArrayKeepingSelector = createSelectorCreator(
  defaultMemoize,
  isShallowArrayEqual
);

// Also wrapping with keepLastIfEqualBy() for good measure.
const createShallowArrayAwareSelector = (...args) =>
  keepLastIfEqualBy(
    isShallowArrayEqual
  )(
    createShallowArrayKeepingSelector(...args)
  );

// Or, if you have lodash available,
import compose from 'lodash/fp/compose';
const createShallowArrayAwareSelector = compose(
  keepLastIfEqualBy(isShallowArrayEqual),
  createSelectorCreator(defaultMemoize, isShallowArrayEqual)
);

这进一步改变了someFunc定义,尽管只是通过改变selectorCreator

const someFunc = createCachedSelector(
  userSelector,
  extendUserDataSelectorSelector,
  (data, $extendUserDataSelector) =>
    data.map($extendUserDataSelector)
)((state, id) => id, {
  selectorCreator: createShallowArrayAwareSelector,
});

其他想法

话虽如此,当您搜索reselect时,您应该尝试查看 npm 中显示的内容re-reselect那里的一些新工具可能对某些情况有用,也可能没有用。不过,您只需重新选择和重新选择,再加上一些额外的功能来满足您的需求,就可以做很多事情。

前言

我遇到了和你一样的情况,不幸的是没有找到一种有效的方法来从另一个选择器的主体调用选择器。

我说的是高效方式,因为你总是可以有一个输入选择器,它传递整个状态(存储),但这会根据每个状态的变化重新计算你的选择器:

const someSelector = createSelector(
   getUserIdsSelector,
   state => state,
   (ids, state) => ids.map((id) => yetAnotherSelector(state, id)
)

方法

但是,对于下面描述的用例,我发现了两种可能的方法。我猜你的情况很相似,所以你可以得到一些见解。

所以情况如下:你有一个选择器,它通过一个 id 从商店中获取一个特定的用户,选择器以特定的结构返回用户。让我们说getUserById选择器。现在一切都很好,尽可能简单。但是当您想通过 id 获取多个用户并重用之前的选择器时,就会出现问题。让我们将其命名为getUsersByIds选择器。

1. 始终使用数组,用于输入 ids 值

第一个可能的解决方案是有一个选择器,它总是需要一个 ids ( getUsersByIds)数组,第二个选择器重用前一个,但它只会得到 1 个用户 ( getUserById)。因此,当您只想从 Store 获取 1 个用户时,您必须使用getUserById,但您必须传递一个只有一个用户 ID 的数组。

这是实现:

import { createSelectorCreator, defaultMemoize } from 'reselect'
import { isEqual } from 'lodash'

/**
 * Create a "selector creator" that uses `lodash.isEqual` instead of `===`
 *
 * Example use case: when we pass an array to the selectors,
 * they are always recalculated, because the default `reselect` memoize function
 * treats the arrays always as new instances.
 *
 * @credits https://github.com/reactjs/reselect#customize-equalitycheck-for-defaultmemoize
 */
const createDeepEqualSelector = createSelectorCreator(
  defaultMemoize,
  isEqual
)

export const getUsersIds = createDeepEqualSelector(
  (state, { ids }) => ids), ids => ids)

export const getUsersByIds = createSelector(state => state.users, getUsersIds,
  (users, userIds) => {
    return userIds.map(id => ({ ...users[id] })
  }
)

export const getUserById = createSelector(getUsersByIds, users => users[0])

用法:

// Get 1 User by id
const user = getUserById(state, { ids: [1] })

// Get as many Users as you want by ids
const users = getUsersByIds(state, { ids: [1, 2, 3] }) 

2. 重用选择器的主体,作为一个独立的函数

这里的想法是将选择器主体的公共部分和可重用部分分开在一个独立的函数中,这样这个函数就可以从所有其他选择器中调用。

这是实现:

export const getUsersByIds = createSelector(state => state.users, getUsersIds,
  (users, userIds) => {
    return userIds.map(id => _getUserById(users, id))
  }
)

export const getUserById = createSelector(state => state.users, (state, props) => props.id, _getUserById)

const _getUserById = (users, id) => ({ ...users[id]})

用法:

// Get 1 User by id
const user = getUserById(state, { id: 1 })

// Get as many Users as you want by ids
const users = getUsersByIds(state, { ids: [1, 2, 3] }) 

结论

方法#1。样板较少(我们没有独立的功能)并且有干净的实现。

方法#2。更可重复使用。想象一下这种情况,当我们调用选择器时我们没有用户的 id,但是我们从选择器的主体中获取它作为一个关系。在这种情况下,我们可以轻松地重用独立功能。这是一个伪示例:

export const getBook = createSelector(state => state.books, state => state.users, (state, props) => props.id,
(books, users, id) => {
  const book = books[id]
  // Here we have the author id (User's id)
  // and out goal is to reuse `getUserById()` selector body,
  // so our solution is to reuse the stand-alone `_getUserById` function.
  const authorId = book.authorId
  const author = _getUserById(users, authorId)

  return {
    ...book,
    author
  }
}

我们使用时所面临的一个问题reselect是,有动态的依赖性跟踪的支持。选择器需要预先声明状态的哪些部分将导致重新计算。

例如,我有一个在线用户 ID 列表和一个用户映射:

{
  onlineUserIds: [ 'alice', 'dave' ],
  notifications: [ /* unrelated data */ ]
  users: {
    alice: { name: 'Alice' },
    bob: { name: 'Bob' },
    charlie: { name: 'Charlie' },
    dave: { name: 'Dave' },
    eve: { name: 'Eve' }
  }
}

我想选择一个在线用户列表,例如[ { name: 'Alice' }, { name: 'Dave' } ].

由于我无法预先知道哪些用户将在线,因此我需要声明对整个state.users商店分支的依赖

示例 1

这有效,但这意味着对无关用户(bob、charlie、eve)的更改将导致重新计算选择器。

我相信这是 reselect 的基本设计选择中的一个问题:选择器之间的依赖关系是静态的。(相比之下,Knockout、Vue 和 MobX 确实支持动态依赖。)

我们遇到了同样的问题,我们想出了@taskworld.com/rereselect. 不是预先和静态地声明依赖项,而是在每次计算期间及时和动态地收集依赖项:

示例 2

这允许我们的选择器对状态的哪一部分可以导致选择器重新计算进行更细粒度的控制。

我做了以下解决方法:

const getSomeSelector = (state: RootState) => () => state.someSelector;
const getState = (state: RootState) => () => state;

const reportDerivedStepsSelector = createSelector(
    [getState, getSomeSelector],
    (getState, someSelector
) => {
    const state = getState();
    const getAnother = anotherSelector(state);
    ...
}

该函数getState永远不会改变,您可以从选择器中获取完整状态而不会破坏选择器备忘录。