模拟代码帮助理解reselect的createSelector函数
react-redux性能优化之reselect
selector 的作用:将多个 state 进行计算后生成新的 state
性能问题
1 | class App extends Component { |
connect 函数实现的时候,我们知道映射 props
的函数被 store.subscribe()
了,因此每次组件更新的时候,无论 state 是否改变,都会调用 mapStateToProps
,而 mapStateToProps
在计算 state 的时候就会调用 state 计算函数,过程 如下:
store.subscribe()
(注册事件)- 状态更新时调用
mapStateToProps
(一个selector,返回 state) - 调用 state 计算函数
selectTodos
那么,问题 来了,如果 selector 的计算量比较大,每次更新的重新计算就会造成性能问题。而解决性能问题的 出发点 就是:避免不必要的计算。
解决问题的方式:从 selector 着手,即 mapStateToProps
,如果 selector 接受的状态参数不变,那么就不调用计算函数,直接利用之前的结果。
- reselect 其实就是 redux 的一个中间件,它通过计算获得新的 state,然后传递到 Redux Store。
- 其主要就是进行了中间的那一步计算,使得计算的状态被缓存,从而根据传入的 state 判断是否需要调用计算函数,而不用在组件每次更新的时候都进行调用,从而更加高效。
不使用Selector:
使用Selector:
采用 reselect,更新 App 组件
1 | // 不用 reselect 的缺点:每次组件更新的时候都会重新计算 visibleTodos |
解释
1 | import { connect } from 'react-redux' |
在上面的例子中,
mapStateToProps
调用getVisibleTodos
去计算todos
.这个函数设计的是相当好的,但是有个缺点:
todos
在每一次组件更新的时候都会重新计算.如果state树的结构比较大,或者计算比较昂贵,每一次组件更新的时候都进行计算的话,将会导致性能问题.Reselect
能够帮助redux来避免不必要的重新计算过程.我们可以使用记忆缓存selector代替
getVisibleTodos
,如果state.todos
和state.visibilityFilter
发生变化,他会重新计算state
,但是发生在其他部分的state变化,就不会重新计算.Reslect提供一个函数
createSelector
来创建一个记忆selectors.createSelector
接受一个input-selectors
和一个变换函数作为参数.- 如果Redux的state发生改变造成
input-selector
的值发生改变,selector会调用变换函数,依据input-selector
做参数,返回一个结果. - 如果
input-selector
返回的结果和前面的一样,那么就会直接返回有关state,会省略变换函数的调用.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31import { createSelector } from 'reselect'
const getVisibilityFilter = (state) => state.visibilityFilter
const getTodos = (state) => state.todos
//下面的函数是经过包装的
export const getVisibleTodos = createSelector(
[ getVisibilityFilter, getTodos ],
(visibilityFilter, todos) => {
switch (visibilityFilter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
)
// 对比之前的getVisibleTodos函数:
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}上面的的实例中,
getVisibilityfilter
和getTodos
是input-selectors.这两个函数是普通的非记忆selector函数,因为他们没有变换他们select的数据.getVisibleTodos
另一方面是一个记忆selector.他接收getVisibilityfilter
和getTodos
作为input-selectors,并且作为一个变换函数计算筛选的todo list.
聚合selectors
一个记忆性selector本身也可以作为另一个记忆性selector的input-selector.这里getVisibleTodos
可以作为input-selector作为关键字筛选的input-selector:
1 | const getKeyword = (state) => state.keyword |
连接一个Selector到Redux Store
如果你正在使用 React Redux, 你可以直接传递selector到 mapStateToProps()
:
1 | import { connect } from 'react-redux' |
reselect缓存规则
1 | let selector = createSelector([fun1, fun2], fun3) |
1 | let someState = selector(state, props) |
- 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的例子:
1 | const makeMapStateToProps = () => { |
1 | import { createSelector } from 'reselect' |
通过结合文章开头的推导代码,你会发现,每个组件的实例的props.id是一定的,因此对应的user也是一定的,那么每次都可以使用缓存起来的user。当然,如果props.id改变来,那么缓存就失效了。
想法2,对makeSelector做深度改造:
1 | let selectors = {} |
在connect的时候,直接在makeSelector的时候传入props.id作为标记,mapStateToProps不再返回函数作为结果。当不再对uid对应的用户进行操作之后,要即时删除这个selector。