使用闭包实现Reselect记忆化选择器是否存在性能损耗?
我有一个关于Reselect选择器记忆化的问题。根据我对Reselect文档的理解,以下是实现带参数记忆化选择器的推荐且正确的方式:
const selectOrdersByCustomer = createSelector( [ state => state.orders, (state, customerId) => customerId, ], (orders, customerId) => { return orders.filter(order => order.customerId === customerId); } );
const orders = useSelector(state => selectOrdersByCustomer(state, customerId));
我的一位同事提出了该选择器的另一种实现方式:
const selectOrdersByCustomer = customerId => createSelector( state => state.orders, orders => { return orders.filter(order => order.customerId === customerId); } );
const orders = useSelector(selectOrdersByCustomer(customerId));
(我对代码进行了简化,实际项目中的实现要复杂得多)我在使用该选择器的组件中添加了console.count('counter');进行测试,发现两种实现触发的重渲染次数相同。我的问题是:第二种实现相比第一种是否存在性能损耗?
回答
第二种实现确实存在隐性的性能损耗,具体体现在这几个方面:
每次渲染都创建新的选择器实例
第二种写法里,selectOrdersByCustomer(customerId)会在组件每次渲染时执行,每次都会调用createSelector生成一个全新的选择器对象。Reselect的记忆化是绑定在选择器实例上的——新实例意味着之前缓存的计算结果完全失效,相当于每次都在重新创建一个无历史缓存的选择器。
对比第一种写法:选择器是全局唯一的单实例,它会缓存所有不同customerId和orders组合的计算结果,只要这两个参数不变,就会直接返回缓存值。useSelector的执行成本更高
useSelector默认会对前后两次返回的结果做引用相等性比较。第二种写法每次传给useSelector的都是新生成的选择器函数(因为每次调用selectOrdersByCustomer都会生成新的createSelector结果),这会导致useSelector每次都必须执行这个新函数,再去比较返回的orders数组。
而第一种写法里,传给useSelector的是一个稳定的箭头函数(只要组件不被销毁重建,这个箭头函数是同一个引用),useSelector可以更高效地复用之前的判断逻辑。缓存粒度缺失导致重复计算
第一种写法的选择器会缓存所有customerId对应的结果,比如用户切换不同客户ID后再切回来,只要orders没变化,就能直接复用缓存。但第二种写法里,每次切换customerId都会生成新的选择器,之前客户ID对应的缓存会被直接丢弃,下次再切回来时必须重新计算。
你测试时重渲染次数相同,是因为console.count只统计了组件的重渲染——组件重渲染的触发条件是useSelector返回的orders引用变化。但第二种写法在每次渲染时都额外增加了创建选择器的开销,同时丢失了跨customerId的缓存能力,这些是你没测到的隐性性能损耗。
内容的提问来源于stack exchange,提问作者frehder

