常见问题解答
为什么当输入状态改变时,我的选择器没有重新计算?
请检查你的记忆化函数是否与你的状态更新函数(例如,如果你正在使用 Redux,那就是减速器)兼容。例如,使用 createSelector
创建的选择器将无法与一个状态更新函数一起工作,该函数会改变现有对象,而不是每次都创建一个新的对象。createSelector
使用身份检查(===
)来检测输入是否已经改变,所以改变一个现有的对象不会触发选择器重新计算,因为改变一个对象并不会改变它的身份。请注意,如果你正在使用 Redux,改变状态对象几乎肯定是一个错误,详情请参考 这里。
为什么当输入状态保持不变时,我的选择器正在重新计算?
要解决你的选择器中意外的重新计算问题,首先确保 inputStabilityCheck
被设置为 'always'
或 'once'
。这个设置有助于通过监视输入的稳定性来进行调试。此外,利用 Output Selector Fields,如 recomputations
、resetRecomputations
、dependencyRecomputations
和 resetDependencyRecomputations
。这些工具有助于识别问题的来源。
请关注 dependencyRecomputations
的计数。如果它在 recomputations
保持不变的情况下增加,这表明你的参数正在改变引用,但你的 input selectors 是稳定的,这通常是期望的行为。
要深入了解,你可以通过使用 argsMemoizeOptions
和 equalityCheck
来确定哪些参数的引用变化过于频繁。考虑以下示例:
- TypeScript
- JavaScript
import { createSelector, lruMemoize } from 'reselect'
export interface RootState {
todos: { id: number; completed: boolean }[]
alerts: { id: number; read: boolean; type: string }[]
}
const selectAlertsByType = createSelector(
[
(state: RootState) => state.alerts,
(state: RootState, type: string) => type
],
(alerts, type) => alerts.filter(todo => todo.type === type),
{
argsMemoize: lruMemoize,
argsMemoizeOptions: {
// 这将检查传递给输出选择器的参数。
equalityCheck: (a, b) => {
if (a !== b) {
console.log('Changed argument:', a, 'to', b)
}
return a === b
}
}
}
)
import { createSelector, lruMemoize } from 'reselect'
const selectAlertsByType = createSelector(
[state => state.alerts, (state, type) => type],
(alerts, type) => alerts.filter(todo => todo.type === type),
{
argsMemoize: lruMemoize,
argsMemoizeOptions: {
// 这将检查传递给输出选择器的参数。
equalityCheck: (a, b) => {
if (a !== b) {
console.log('Changed argument:', a, 'to', b)
}
return a === b
}
}
}
)
我可以在没有Redux的情况下使用Reselect吗?
可以。Reselect没有依赖任何其他包,所以虽然它是设计用来和Redux一起使用的,但它也可以独立使用。只要数据是以不可变的方式更新的,它就可以用于任何普通的JS数据,比如典型的React状态值。
如何创建一个接受参数的选择器?
你提供的每一个input selectors都会被调用,并传入所有的选择器参数。你可以添加额外的输入选择器来提取参数,并将它们转发给result function,像这样:
const selectTodosByCategory = createSelector(
(state: RootState) => state.todos,
// 提取第二个参数以便传递
(state: RootState, category: string) => category,
(todos, category) => todos.filter(t => t.category === category)
)
在Reselect中创建一个接受参数的选择器时,重要的是要正确地构造你的输入和输出选择器。以下是需要考虑的关键点:
-
参数的一致性:确保所有位置参数在input selectors中的类型都是一致的。
-
选择性使用参数:设计每个选择器只使用其相关的参数,并忽略其余的。这是非常重要的,因为所有的input selectors都会接收到传递给output selector的相同参数。
假设我们有以下的状态结构:
interface RootState {
items: {
id: number
category: string
vendor: { id: number; name: string }
}[]
// ... 其他状态属性 ...
}
要创建一个基于category
过滤items
并排除特定id
的选择器,你可以按照以下方式设置你的选择器:
const selectAvailableItems = createSelector(
[
// 第一个输入选择器从状态中提取items
(state: RootState) => state.items,
// 第二个输入选择器转发category参数
(state: RootState, category: string) => category,
// 第三个输入选择器转发ID参数
(state: RootState, category: string, id: number) => id
],
// 输出选择器使用提取的items,category和ID
(items, category, id) =>
items.filter(item => item.category === category && item.id !== id)
)
在内部,Reselect正在做这个:
// 输入选择器 #1
const items = (state: RootState, category: string, id: number) => state.items
// 输入选择器 #2
const category = (state: RootState, category: string, id: number) => category
// 输入选择器 #3
const id = (state: RootState, category: string, id: number) => id
// 输出选择器的结果
const finalResult =
// 结果函数
items.filter(item => item.category === category && item.id !== id)
在这个例子中,selectItemId
期望它的第二个参数是一些简单的值,而selectVendorName
期望第二个参数是一个对象。如果你调用selectItemById(state, 42)
,selectVendorName
会出错,因为它试图访问42.name
。Reselect的TS类型应该能检测到这个并阻止编译:
const selectItems = (state: RootState) => state.items
// 期望第二个参数是一个数字
const selectItemId = (state: RootState, itemId: number) => itemId
// 期望第二个参数是一个对象
const selectVendorName = (
state: RootState,
vendor: { id: number; name: string }
) => vendor.name
const selectItemById = createSelector(
[selectItems, selectItemId, selectVendorName],
(items, itemId, vendorName) => items[itemId]
)
可以自定义 memoization 行为吗?
可以。内置的 lruMemoize
记忆器对于许多用例都非常有效,但是它可以被自定义或替换为不同的记忆器。请参阅这些示例。