Reselect 中闭包式 memoized selector 的性能陷阱解析

使用闭包动态创建 reselect selector(如 `selectbycustomer(id)`)会导致每次渲染都新建一个独立的 memoized 函数,无法复用缓存,造成重复计算和内存浪费;而标准的参数化 selector(接受 `(state, props)`)共享同一实例,真正实现高效 memoization。

在 React + Redux 应用中,Reselect 是优化派生状态计算的关键工具。但 selector 的写法直接影响性能表现。两种常见模式看似等价,实则存在本质差异:

推荐方式:参数化 selector(单实例 + 多参数)

const selectOrdersByCustomer = createSelector(
  state => state.orders,
  (state, customerId) => customerId, // 第二个输入选择器接收 props 参数
  (orders, customerId) => orders.filter(order => order.customerId === customerId)
);

// 在组件中使用:
const orders = useSelector(state => selectOrdersByCustomer(state, customerId));

该方式仅创建一个 selector 实例,其内部缓存(recomputations()、lastResult() 等)全程复用。只要 state.orders 和 customerId 均未变化,输出选择器(output selector)完全跳过执行,直接返回缓存结果。

危险方式:闭包式 selector 工厂(每次渲染新建实例)

const selectOrdersByCustomer = customerId => 
  createSelector(
    state => state.orders,
    orders => orders.filter(order => order.customerId === customerId)
  );

// 在组件中使用(⚠️ 错误!):
const orders = useSelector(selectOrdersByCustomer(customerId)); // 每次渲染都调用 factory!

此处 selectOrdersByCustomer(customerId) 每次组件渲染都会返回一个全新的 selector 函数。即使 customerId 相同,每个 selector 都拥有独立的缓存空间,且无任何共享机制。结果是:

  • 输出选择器被反复执行(如示例中 console.count 触发 3 次);
  • 缓存失效,=== 比较返回 false,导致不必要的对象重建;
  • 内存中堆积大量冗余 selector 实例,增加 GC 压力。

? 关键验证:recomputations() 与引用相等性
通过日志可清晰验证差异:

  • 参数化 selector:recomputations() 返回 1,三次调用结果严格 ===;
  • 闭包 selector:三次调用触发三次 console.count,且 s1 === s2 为 false —— 每次都生成新数组。

? 正确使用闭包的场景(极少数)
若确需工厂模式(如动态配置 selector),必须确保 selector 实例稳定复用,例如:

// ✅ 正确:useMemo 缓存 selector 实例
const selectByCustomer = useMemo(
  () => createSelector(
    state => state.orders,
    orders => orders.filter(o => o.customerId === customerId)
  ),
  [customerId] // 依赖变更时才重建 selector
);
const orders = useSelector(selectByCustomer);

但即便如此,仍不如原生参数化 selector 简洁、可靠、符合 Reselect 设计哲学。

? 总结建议

  • 优先采用 (state, props) 形式的参数化 selector,它是 Reselect 官方推荐且经过充分优化的模式;
  • 避免在 useSelector 内联调用 selector 工厂函数;
  • 使用 selector.recomputations() 和 selector.lastResult() 辅助调试缓存行为;
  • 对高频更新的 selector,可结合 createStructuredSelector 或 defaultMemoize 的 equalityCheck 进行深度定制。

性能优化始于正确的抽象——不是“用了 memoization”,而是“用对了 memoization”。