模拟代码帮助理解reselect的createSelector函数
react-redux性能优化之reselect
selector 的作用:将多个 state 进行计算后生成新的 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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| class App extends Component { static propTypes = {}; render() { const { visibleTodos, visibilityFilter, onAddClick, onTodoClick, onFilterChange } = this.props; return ( <div> <AddTodo onAddClick={(text) => onAddClick(text)} />
<TodoList todos={visibleTodos} onTodoClick={(index) => onTodoClick(index)} />
<Footer filter={visibilityFilter} onFilterChange={(nextFilter) => onFilterChange(nextFilter)} /> </div> ); } }
export const selectTodos = (todos, filter) => { switch (filter) { case 'SHOW_ALL': return todos case 'SHOW_COMPLETED': return todos.filter(todo => todo.completed) case 'SHOW_ACTIVE': return todos.filter(todo => !todo.completed) } }
const mapStateToProps = (state) => ({ visibleTodos: selectTodos(state.todos, state.visibilityFilter), visibilityFilter: state.visibilityFilter });
const mapDispatchToProps = (dispatch) => ({ onAddClick: (text) => dispatch(addTodo(text)), onTodoClick: (index) => dispatch(toggleTodo(index)), onFilterChange: (nextFilter) => dispatch(setVisibilityFilter(nextFilter)) });
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
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 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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
|
const getTodos = (state) => state.todos; const getVisibilityFilter = (state) => state.visibilityFilter;
export const selectTodos = (todos, filter) => { switch (filter) { case 'SHOW_ALL': return todos case 'SHOW_COMPLETED': return todos.filter(todo => todo.completed) case 'SHOW_ACTIVE': return todos.filter(todo => !todo.completed) } }
const getVisibleTodos = createSelector( [getTodos, getVisibilityFilter], selectTodos );
const mapStateToProps = (state) => ({ visibleTodos: getVisibleTodos(state), visibilityFilter: state.visibilityFilter });
const mapDispatchToProps = (dispatch) => ({ onAddClick: (text) => dispatch(addTodo(text)), onTodoClick: (index) => dispatch(toggleTodo(index)), onFilterChange: (nextFilter) => dispatch(setVisibilityFilter(nextFilter)) });
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
解释
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 31 32 33 34 35 36 37
| import { connect } from 'react-redux' import { toggleTodo } from '../actions' import TodoList from '../components/TodoList'
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) } }
const mapStateToProps = (state) => { return { todos: getVisibleTodos(state.todos, state.visibilityFilter) } }
const mapDispatchToProps = (dispatch) => { return { onTodoClick: (id) => { dispatch(toggleTodo(id)) } } }
const VisibleTodoList = connect( mapStateToProps, mapDispatchToProps )(TodoList)
export default VisibleTodoList
|
在上面的例子中,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 31
| import { 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) } } )
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 2 3 4 5 6 7 8
| const getKeyword = (state) => state.keyword
const getVisibleTodosFilteredByKeyword = createSelector( [ getVisibleTodos, getKeyword ], (visibleTodos, keyword) => visibleTodos.filter( todo => todo.text.indexOf(keyword) > -1 ) )
|
连接一个Selector到Redux Store
如果你正在使用 React Redux, 你可以直接传递selector到 mapStateToProps():
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
| import { connect } from 'react-redux' import { toggleTodo } from '../actions' import TodoList from '../components/TodoList' import { getVisibleTodos } from '../selectors'
const mapStateToProps = (state) => { return { todos: getVisibleTodos(state) } }
const mapDispatchToProps = (dispatch) => { return { onTodoClick: (id) => { dispatch(toggleTodo(id)) } } }
const VisibleTodoList = connect( mapStateToProps, mapDispatchToProps )(TodoList)
export default VisibleTodoList
|
reselect缓存规则
1
| let selector = createSelector([fun1, fun2], fun3)
|
1 2 3 4 5 6 7 8 9 10
| let someState = selector(state, props)
let someState = (function(state, props) { let state1 = fun1(state, props) let state2 = fun2(state, props) return fun3(state1, state2) })(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 2 3 4 5 6 7 8 9 10
| const makeMapStateToProps = () => { const getSelector = makeSelector() const mapStateToProps = (state, props) => { return { todos: getSelector(state, props) } } return mapStateToProps } export default connect(makeMapStateToProps)(MyComponent)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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做深度改造:
1 2 3 4 5 6 7 8
| 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。