Reselect 中使用闭包实现带参选择器的性能陷阱与正确用法

在 reselect 中,通过闭包(如 `customerid => createselector(...)`)创建带参数的选择器会导致每次渲染都生成新实例,使 memoization 完全失效,引发重复计算和内存浪费;而官方推荐的多参数 `createselector` 方式才能真正复用缓存。

Reselect 的核心价值在于共享、可复用、带缓存的选择器实例。其 memoization 机制依赖于两个关键前提:

  1. 选择器函数是稳定引用(即不随渲染重新创建)
  2. 输入参数能被正确捕获并参与缓存键计算

❌ 错误方式:闭包工厂模式(性能陷阱)

// 危险!每次调用 selectOrdersByCustomer(customerId) 都创建全新 selector 实例
const selectOrdersByCustomer = customerId =>
  createSelector(
    state => state.orders,
    orders => {
      console.count('⚠️ output selector executed'); // 每次渲染都触发!
      return orders.filter(order => order.customerId === customerId);
    }
  );

// 在组件中:
const orders = useSelector(selectOrdersByCustomer(customerId));

该写法看似简洁,实则违背 Reselect 设计原则:

  • selectOrdersByCustomer(customerId) 每次调用返回一个全新的 createSelector 实例
  • 每个实例拥有独立的缓存(recomputations()、memoizedResultSelector),互不共享;
  • 即使 state.orders 和 customerId 均未变化,因 selector 实例不同,缓存永远无法命中 → 输出选择器反复执行,且返回新数组(=== false)
✅ 实验验证(见原答案日志):三次调用 selectOrdersByCustomer2(1)(state) 导致输出选择器执行 3 次,且结果引用不等(false false)。

✅ 正确方式:多参数选择器(推荐标准写法)

// ✅ 稳定声明:单个 selector 实例,支持参数化输入
const selectOrdersByCustomer = createSelector(
  state => state.orders,
  (state, customerId) => customerId, // 第二个输入选择器,接收 props 参数
  (orders, customerId) => {
    console.count('✅ memoized output selector'); // 仅当 orders 或 customerId 变化时执行
    return orders.filter(order => order.customerId === customerId);
  }
);

// 在组件中(useSelector 自动透传第二个参数):
const orders = useSelector(state => selectOrdersByCustomer(state, customerId));

此方式优势显著:

  • selectOrdersByCustomer 是单一、稳定的函数引用,可安全定义在模块顶层;
  • Reselect 内部将 (state, customerId) 作为缓存键(equalityCheck 默认为 ===),确保相同参数组合命中缓存;
  • 多次调用 selectOrdersByCustomer(state, 1) 仅首次执行过滤逻辑,后续直接返回缓存结果(=== true)。

⚠️ 注意事项与最佳实践

  • 避免在组件内部或 useSelector 回调中动态创建 selector(包括闭包、useMemo(() => createSelector(...)) 等);
  • 若需动态生成 selector(极少数场景),应配合 useMemo 缓存 factory 函数本身,并确保 customerId 是 useMemo 的依赖项——但通常没必要,优先用多参数模式;
  • 对于复杂参数(如对象),注意 Reselect 默认浅比较(===),必要时自定义 memoize 或 equalityCheck;
  • 使用 createSelectorCreator + lodash.memoize 可扩展缓存策略,但默认方案已满足绝大多数场景。

总结

闭包式 selector 不是“稍慢一点”,而是彻底丢失 memoization 能力——它把 Reselect 降级为普通纯函数,还额外增加了 selector 创建开销。务必坚持 Reselect 官方范式:参数通过输入选择器传入,selector 实例全局唯一、稳定复用。这是保障 React-Redux 应用高性能的关键细节之一。