【问题标题】:Reselect - selector that invokes another selector?重新选择 - 调用另一个选择器的选择器?
【发布时间】:2018-11-30 13:52:54
【问题描述】:

我有一个选择器:

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));
}

而且确实有诀窍,但它不只是一种解决方法吗?

【问题讨论】:

  • 您能否在问题中添加您的动机?

标签: javascript reactjs reselect


【解决方案1】:

为您的 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));
}

这基本上就是re-reselect 所做的。如果您计划在全局级别实施 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 基准数据的函数,您可以保持其他选择器的接口干净。

编辑:关于收藏

上述实现目前易受攻击的一件事是,如果extendUserDataSelectorSelector 的输出发生变化,因为它自己的依赖选择器发生了变化,但userSelector 获得的用户数据没有变化,由 userSelector 创建的实际计算实体也没有变化extendUserDataSelectorSelector。在这些情况下,您需要做两件事:

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

编辑:避免大量缓存

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

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

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

编辑 2(或者我猜是 4?):重新关注收藏 pt。 1:Multi-Memoizing 扩展器

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

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

基本上,您需要一个 Multi-Memoizer,它的作用方式与 re-reselect 对选择器的作用基本相同。 事实上,让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 本身可能不会改变,但其中的 ids 指向的实体可能具有。
  2. arrayOfSomeIds 可能是内存中的新对象,但实际上具有相同的 id。
  3. arrayOfSomeIds 没有改变,但包含被引用实体的集合确实发生了变化,而这些特定 id 引用的特定实体没有改变。

这就是为什么我主张将数组(和其他集合!)的扩展/扩展/具体化/whateverelseification 委托给尽可能晚的数据获取-导出-视图-渲染过程:这是一个痛苦杏仁核必须考虑所有这些。

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

从上面缓存的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,这可能已经足够了。但是,有时您可能还需要 catch case 2 来检查输入数组。 这可以通过使用重新选择的createSelectorCreatormake our own createSelector using a custom equality function 来实现

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,
});

其他想法

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

【讨论】:

  • 你的 reify 看起来很有趣。我最喜欢它,因为您不会在父选择器中公开任何内容。但是,您提到的集合限制对于选择/扩展单个用户也是有效的(我猜)。想象一下,如果在您的扩展器中,您使用其相关国家/地区扩展用户。如果 Country 更改,则不会重新计算父 User 选择器。你提到了两种可能的策略,但我无法想象它们。你能展示一个简短的代码示例吗?谢谢!
  • 嘿,我添加了几个部分来扩展这两点!希望它们很容易理解,因为它主要只是添加额外的包装工具。对于您对用户和相关国家/地区的其他关注,只要国家/地区是单个关系,选择器缓存应该正常工作,如果用户有多个国家/地区需要额外的工作。
  • 我在不同的几天里多次审查了你的答案,以便深入理解这个想法。所以我理解了这个想法,这是有道理的,但对我来说看起来太复杂了。对于这样一个简单的任务 - 我们必须创建和管理这样的复杂性。我感谢您的努力,并可能会尝试您的方法。已经为你的答案投票了! :)
  • 哈哈哈,很高兴你从中学到了一些东西!我自己花了相当长的时间和多次尝试才真正找到这些解决方案,这就是为什么我通常建议人们在 connect() 中创建特定于组件的选择器,而不是尝试在全局范围内处理所有内容的缓存;涵盖所有情况非常复杂,并且在绝对需要之前不派生数据来避免它们。祝你好运,无论你采取哪种方法!
  • 这里有点晚了,但我认为@JosephSikorski 提出的解决方案的真正聪明之处主要归结为通过中间步骤绑定商店的想法。这解决了子选择器无法访问状态的问题,同时也不会导致缓存破坏问题,因为返回的函数签名应该保持静态。关于这个答案的唯一不幸部分是我认为可能有太多细节,并且提出的简单解决方案有点丢失。除此之外,非常感谢您的好主意,我将继续以reifier 模式传播它!
【解决方案2】:

前言

我遇到了和你一样的情况,很遗憾没有找到一种有效的方法从另一个选择器的主体中调用一个选择器。

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

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

方法

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

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

1。始终使用数组作为输入 id 值

第一个可能的解决方案是有一个选择器,它总是需要一个 id 数组 (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
  }
}

【讨论】:

  • 你能看看我的编辑吗?我在那里编写的那个功能正在杀死我的应用程序。不知道该怎么办。
  • [re-reselect](https://github.com/toomuchdesign/re-reselect) 使用相同的按键记忆方法。 However in your custom implementation, you would have problems, when something changed in the selector extendUserDataSelector state, because the parent selector won't be recomputed.您可以将re-reselectreselect 与我的一种可能方法结合使用。
【解决方案3】:

我们在使用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 分支的依赖:

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

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

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

这让我们的选择器可以更细粒度地控制状态的哪一部分会导致选择器被重新计算。

【讨论】:

    【解决方案4】:

    我做了以下解决方法:

    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 函数永远不会改变,您可以从选择器中获取完整状态,而不会破坏选择器备忘录。

    【讨论】:

      【解决方案5】:

      Recompute 是 reselect 的替代方法,它实现了动态依赖跟踪并允许将任意数量的参数传递给选择器,您可以检查这是否可以解决您的问题

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2020-07-31
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-05-16
        • 2021-10-02
        • 1970-01-01
        • 2017-03-10
        相关资源
        最近更新 更多