模拟代码帮助理解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>
);
}
}

// 一个 state 计算函数
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)
}
}

// mapStateToProps 就是一个 selector,每次组件更新的时候就会被调用
// 【缺点】每次组件更新的时候都会重新计算 visibleTodos,如果计算量比较大,会造成性能问题
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 是否改变,都会调用 mapStateToPropsmapStateToProps 在计算 state 的时候就会调用 state 计算函数,过程 如下:

  1. store.subscribe()(注册事件)
  2. 状态更新时调用 mapStateToProps(一个selector,返回 state)
  3. 调用 state 计算函数 selectTodos

那么,问题 来了,如果 selector 的计算量比较大,每次更新的重新计算就会造成性能问题。而解决性能问题的 出发点 就是:避免不必要的计算

解决问题的方式:从 selector 着手,即 mapStateToProps如果 selector 接受的状态参数不变,那么就不调用计算函数,直接利用之前的结果。

  • reselect 其实就是 redux 的一个中间件,它通过计算获得新的 state,然后传递到 Redux Store。
  • 其主要就是进行了中间的那一步计算,使得计算的状态被缓存,从而根据传入的 state 判断是否需要调用计算函数,而不用在组件每次更新的时候都进行调用,从而更加高效。

不使用Selector:

WechatIMG55

使用Selector:

WechatIMG56

采用 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
// 不用 reselect 的缺点:每次组件更新的时候都会重新计算 visibleTodos
/*const mapStateToProps = (state) => ({
visibleTodos: selectTodos(state.todos, state.visibilityFilter),
visibilityFilter: state.visibilityFilter
});*/

// 采用 reselect 后,相当于记忆缓存,会缓存状态
// 如果 state.todos 和 state.visibilityFilter 发生变化,它会重新计算 state
// 但是发生在其他部分的 state 变化,就不会重新计算
const getTodos = (state) => state.todos;
const getVisibilityFilter = (state) => state.visibilityFilter;

// reducer
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)
}
}

// getTodos 和 getVisibilityFilter 返回的参数将传入 selectTodos
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))
});


/**
* connect用法:connect(selectors)(App); // selectors 即是一个对象,包含了状态属性和方法
* connect作用:即使 Dumb 组件从 store 中获取数据
*/
// export default connect(mapStateToProps)(App);
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'

//下面这段代码是根据过滤器的state来改变日程state的函数
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是根据过滤函数返回的state,传入两个实参
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
//mapDispatchToProps来传递dispatch的方法
const mapDispatchToProps = (dispatch) => {
return {
onTodoClick: (id) => {
dispatch(toggleTodo(id))
}
}
}
//使用Redux的connect函数注入state,到TodoList组件
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)

export default VisibleTodoList
  • 在上面的例子中,mapStateToProps调用getVisibleTodos去计算todos.这个函数设计的是相当好的,

  • 但是有个缺点:todos在每一次组件更新的时候都会重新计算.如果state树的结构比较大,或者计算比较昂贵,每一次组件更新的时候都进行计算的话,将会导致性能问题.

  • Reselect能够帮助redux来避免不必要的重新计算过程.

  • 我们可以使用记忆缓存selector代替getVisibleTodos,如果state.todosstate.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)
    }
    }
    )

    // 对比之前的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)
    }
    }
    • 上面的的实例中,getVisibilityfiltergetTodos是input-selectors.这两个函数是普通的非记忆selector函数,因为他们没有变换他们select的数据.

    • getVisibleTodos另一方面是一个记忆selector.他接收getVisibilityfiltergetTodos作为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)

// 在mapStateToProps中这样用推导出
// 在使用selector的时候,把它当作一个函数,传入state和props作为参数。
// 这样就可以非常容易的理解,createSelector传入的参数中的函数,各自在什么时候执行,执行的结果拿来干什么。
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,参数相同的情况下,得到的结果永远相同。有两种解决的想法:

  1. 为每一个组件设置单独的映射,这个可以通过react-redux的connect来实现,当mapStateToProps返回的是一个函数时,那么这个函数的运算结果仅对组件的当前实例生效,也就是说,在我们写mapStateToProps函数时,不能直接返回映射关系,而是返回一个函数,这个函数里面去做一些处理后再返回映射关系。下面有例子。
  2. 既然fun3的计算结果是根据参数来缓存的,那么我们可以尝试对参数做hash,固定的参数对应固定的fun3函数体,不同的参数对应不同的fun3函数体,当在不同的参数之间切换时,如果发现这个hash有存在的fun3函数体,那么就立即用它的缓存。下面也有例子。

想法1的例子:

1
2
3
4
5
6
7
8
9
10
const makeMapStateToProps = () => {
const getSelector = makeSelector() // 下一段代码看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。