reselect提供的createSelector API具有记忆功能,但是它的用法奇怪,让入门者比较难理解,我也废了不少力才理解到这块。现在通过模拟脚本,来协助理解。先来看下我们怎么创建一个selector函数:
let selector = createSelector([fun1, fun2], fun3)
在使用selector的时候,把它当作一个函数,传入state和props作为参数。接下来我们通过selector的反推来理解:
let someState = selector(state, props) <- 在mapStateToProps中这样用推导出 => let someState = (function(state, props) { let state1 = fun1(state, props) let state2 = fun2(state, props) return fun3(state1, state2) })(state, props)
这样就可以非常容易的理解,createSelector传入的参数中的函数,各自在什么时候执行,执行的结果拿来干什么。
createSelector的第一个参数是个数组,数组的个数和第二个参数函数(fun3)的参数个数相同,这个怎么做到呢?其实很简单,用扩展运算符...或者用.apply都可以做到。
最后的问题是,如何做到记忆功能呢?
reselect的记忆功能的规则是,fun3的实参如果不变,那么说明它的运算结果也不变,可以直接返回缓存起来的结果。
所以,要使记忆功能生效,你必须保证fun3的实参不变,说白了,就是fun1, fun2的计算结果不变,因此fun1, fun2必须是返回固定值的函数。这种函数比pure function还要硬性,即使参数不同,也要永远返回一个值。当然,我们是不可能做到这样的,如果fun1依赖的state发生来变化,那么它的结果自然就会变,这个时候,fun3就不再返回缓存,而是重新计算结果,同时缓存新的结果,下次就可以用这个缓存了。这样,就做到selector的响应式。
最后的问题是,如果fun1, fun2的结果会随着props的不同而返回不同的结果呢?这种情况普遍存在,一个react组件可能在一个页面里面被多次使用,每次使用的时候props可能不同。这就会导致reselect的记忆功能失效。
解决的办法还是要从记忆功能的原理中去寻找。
每一个计算结果的缓存,与传入fun3的参数是一一对应的,fun3可以说是一个pure function,参数相同的情况下,得到的结果永远相同。有两种解决的想法:
- 为每一个组件设置单独的映射,这个可以通过react-redux的connect来实现,当mapStateToProps返回的是一个函数时,那么这个函数的运算结果仅对组件的当前实例生效,也就是说,在我们写mapStateToProps函数时,不能直接返回映射关系,而是返回一个函数,这个函数里面去做一些处理后再返回映射关系。下面有例子。
- 既然fun3的计算结果是根据参数来缓存的,那么我们可以尝试对参数做hash,固定的参数对应固定的fun3函数体,不同的参数对应不同的fun3函数体,当在不同的参数之间切换时,如果发现这个hash有存在的fun3函数体,那么就立即用它的缓存。下面也有例子。
想法1的例子:
const makeMapStateToProps = () => { const getSelector = makeSelector() // 下一段代码看makeSelector是怎么写的 const mapStateToProps = (state, props) => { return { todos: getSelector(state, props) } } return mapStateToProps } export default connect(makeMapStateToProps)(MyComponent)
import { createSelector } from 'reselect' function getSelector(state, props) { return state[props.id] } export function makeSelector() { return createSelector( [ getSelector ], (user) => { user.total_books = user.books.length return user }, ) }
通过结合文章开头的推导代码,你会发现,每个组件的实例的props.id是一定的,因此对应的user也是一定的,那么每次都可以使用缓存起来的user。当然,如果props.id改变来,那么缓存就失效了。
想法2,对makeSelector做深度改造:
let selectors = {} function makeSelector(uid) { if (selectors[uid]) return selectors[uid] let selector = createSelector(...) } function deleteSelector(uid) { delete selectors[uid] }
在connect的时候,直接在makeSelector的时候传入props.id作为标记,mapStateToProps不再返回函数作为结果。当不再对uid对应的用户进行操作之后,要即时删除这个selector。