对于您的 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
};
}
);
Then someFunc
会成为:
const someFunc = createSelector(
userSelector,
extendUserDataSelectorSelector,
// I prefix injected functions with a $.
// It's not really necessary.
(data, $extendUserDataSelector) =>
data.map($extendUserDataSelector)
);
我将其称为具体化模式,因为它创建了一个预先绑定到当前状态的函数,并且该函数接受单个输入并将其具体化。我通常用它来通过 id 获取东西,因此使用“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));
}
基本上就是这样重新选择 https://www.npmjs.com/package/re-reselect做。如果您计划在全球范围内实施每个 id 记忆化,您可能需要考虑这一点。
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);
或者你可以用弓包裹你的记忆多选择器创建器并调用它createCachedSelector
,因为它基本上是同一件事。
编辑:为什么返回函数
您可以执行此操作的另一种方法是仅选择运行该程序所需的所有适当数据extendUserDataSelector
计算,但这意味着将想要使用该计算的所有其他函数暴露给其接口。通过返回一个只接受单个的函数user
基本数据,您可以保持其他选择器的界面干净。
编辑:关于收藏
上述实现目前容易受到的一件事是如果extendUserDataSelectorSelector
的输出发生变化,因为它自己的依赖选择器发生了变化,但是通过获取的用户数据userSelector
没有改变,并且由创建的实际计算实体也没有改变extendUserDataSelectorSelector
。在这些情况下,您需要做两件事:
- 多重记忆功能
extendUserDataSelectorSelector
返回。我建议将其提取到一个单独的全局记忆函数中。
- Wrap
someFunc
这样,当它返回一个数组时,它会将该数组与之前的结果逐元素进行比较,如果它们具有相同的元素,则返回之前的结果。
编辑:避免太多缓存
全局级别的缓存当然是可行的,如上所示,但如果您考虑其他几个策略来解决该问题,则可以避免这种情况:
- 不要急于扩展数据,而是将其推迟到实际渲染数据本身的每个 React(或其他视图)组件。
- 不要急于将 ids/base-objects 列表转换为扩展版本,而是让父母将这些 ids/base-objects 传递给孩子们。
我一开始并没有在我的一个主要工作项目中遵循这些原则,但愿我做到了。事实上,我后来不得不采用全局记忆路线,因为这比重构所有视图更容易修复,这是应该做的事情,但我们目前缺乏时间/预算。
编辑 2(或者我猜是 4?):重新考虑 Collections pt。 1:多重记忆扩展器
注意:在完成这一部分之前,假设传递给扩展器的基本实体将具有某种类型id
可以用来唯一地识别它的属性,或者可以从它廉价地衍生出某种类似的属性。
为此,您可以以类似于任何其他选择器的方式记忆扩展器本身。但是,由于您希望 Extender 记住其参数,因此您不希望将 State 直接传递给它。
基本上,您需要一个 Multi-Memoizer,其作用方式与重新选择 https://www.npmjs.com/package/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
)
);
That extendUserData
是真正的缓存发生的地方,但公平警告:如果你有很多baseUser
实体,它可能会变得相当大。
编辑 2(或者我猜是 4?):重新考虑 Collections pt。 2:数组
数组是缓存存在的祸根:
-
arrayOfSomeIds
本身可能不会改变,但 id 所指向的实体可以具有。
-
arrayOfSomeIds
可能是内存中的一个新对象,但实际上具有相同的 id。
-
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,这可能已经足够好了。然而,有时您可能还需要 catch case 2,检查输入数组。
这可以通过使用重新选择来实现createSelectorCreator
to 自己做createSelector使用自定义相等函数 https://www.npmjs.com/package/reselect#customize-equalitycheck-for-defaultmemoize
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,
});
其他想法
总而言之,当您搜索时,您应该尝试看看 npm 中显示的内容reselect
and re-reselect
。那里的一些新工具可能对某些情况有用,也可能没有用。不过,您只需重新选择和重新重新选择,再加上一些额外的功能来满足您的需求,就可以做很多事情。