Reselect

作者: 雨棚 | 来源:发表于2017-07-19 16:05 被阅读0次

    一个简单的redux的selector库

    • 可以计算数据来源,允许redux只保存最小的必须数据量
    • 快速高效,除非selector的参数发生变化,不然selector不会重新计算
    • 可组合,可以作为别的selector的输入
    Example

    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 tree非常大,或者算计算成本 很高,这种重复计算会导致一些性能问题。Reselector能够帮助避免多余的重复计算。

    创建一个可以记忆的Selector

    state.todos或者state.visibilityFilter改变时,我们用一个带记忆功能的selector来代替getVisibleTodos来重新计算todos,但是state tree中不相关部分更新时,我们不会重新计算。
    Reselector提供了createSelector来创建可记忆的selector。createSelector接收一个数组的input-selector和一个transform function作为参数,如果Redux的state tree发生改变并导致了input-selector发生变化,这个可记忆的selector就会调用transform function(以input-selector作为参数)然后返回结果。如果input-selector的数据没有变化就会返回上一次的计算值,不调用transform。

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

    在上面的例子中 getVisiblityFiltergetTodos是input-selector。他们创建了普通的不可记忆selector因为他们没有改变他们所选的state。另一方面getvisibleTodos则是一个可记忆selector。它接收了getVisibilityFiltergetTodos和transform函数作为参数。

    组合Selector

    可记忆selector本身可以是另一个可记忆selector的参数,下面是getVisibleTodos作为input-selector参数的例子。

    const getKeyword = (state) => state.keyword
    
    const getVisibleTodosFilteredByKeyword = createSelector(
      [ getVisibleTodos, getKeyword ],
      (visibleTodos, keyword) => visibleTodos.filter(
        todo => todo.text.includes(keyword)
      )
    )
    
    连接selector和redux store

    如果你使用React Redux,你可以像平时一样在mapStateToProps里调用selector。

    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
    

    在Selector里使用React Props

    这一节在app里引入了一个假设的拓展来支持多重Todo List。要注意一个拓展的完整实现要求reducer、components、actions等的改变,但是这些不是我们要讨论的重点,为了简洁我们忽略了这部分。

    目前为止我们只看到selector接受Redux store state作为参数,但是其实slector也可以接受props。
    这是一个叫App的组件,用来渲染三个VisibleTodoList组件,其中每个组件都有一个叫listId的prop:

    import React from 'react'
    import Footer from './Footer'
    import AddTodo from '../containers/AddTodo'
    import VisibleTodoList from '../containers/VisibleTodoList'
    
    const App = () => (
      <div>
        <VisibleTodoList listId="1" />
        <VisibleTodoList listId="2" />
        <VisibleTodoList listId="3" />
      </div>
    )
    

    每个visibleTodoList容器应该根据listId使用state中不同的部分,让我们来修改getVisibilityFiltergetTodos来接收一个props作为参数。

    import { createSelector } from 'reselect'
    
    const getVisibilityFilter = (state, props) =>
      state.todoLists[props.listId].visibilityFilter
    
    const getTodos = (state, props) =>
      state.todoLists[props.listId].todos
    
    const getVisibleTodos = createSelector(
      [ getVisibilityFilter, getTodos ],
      (visibilityFilter, todos) => {
        switch (visibilityFilter) {
          case 'SHOW_COMPLETED':
            return todos.filter(todo => todo.completed)
          case 'SHOW_ACTIVE':
            return todos.filter(todo => !todo.completed)
          default:
            return todos
        }
      }
    )
    
    export default getVisibleTodos
    

    可以从mapStateToProps的第二个参数中获取props

    const mapStateToProps = (state, props) => {
      return {
        todos: getVisibleTodos(state, props)
      }
    }
    

    现在getVisibleTodos能够使用props了,并且运行得很好。

    但是有个一问题!

    如果使用包含多个VisibleTodoList容器实例的getVisibleTodos函数不能正确的记忆state是否发生变化。

    import { connect } from 'react-redux'
    import { toggleTodo } from '../actions'
    import TodoList from '../components/TodoList'
    import { getVisibleTodos } from '../selectors'
    
    const mapStateToProps = (state, props) => {
      return {
        // WARNING: THE FOLLOWING SELECTOR DOES NOT CORRECTLY MEMOIZE
        todos: getVisibleTodos(state, props)
      }
    }
    
    const mapDispatchToProps = (dispatch) => {
      return {
        onTodoClick: (id) => {
          dispatch(toggleTodo(id))
        }
      }
    }
    
    const VisibleTodoList = connect(
      mapStateToProps,
      mapDispatchToProps
    )(TodoList)
    
    export default VisibleTodoList
    

    一个通过createSelector的生成的selector有size为1的缓存,并且它接收的一系列参数相同时,只会返回被缓存的值。如果我们在<VisibleTodoList listId="1" /><VisibleTodoList listId="2" />之间切换时,这个共用的selector会不断的接收不同的props参数({listId:1}{listId:2}),这会导致selector每次都重新计算而不是返回缓存值,因为接收的props参数不同。下一节我们会解决这个问题。

    共用Selector时,传入不同props依旧使用缓存的方法

    下面的例子要求React Redux v4.3.0或以上
    另外一个办法是re-reselect

    解决办法是,每个VisibleTodoList组件都使用专属的一个selector的copy。
    我们创建一个叫makeGetVisibleTodos的函数,每次调用时返回一个新的getVisibleTodos的copy。

    import { createSelector } from 'reselect'
    
    const getVisibilityFilter = (state, props) =>
      state.todoLists[props.listId].visibilityFilter
    
    const getTodos = (state, props) =>
      state.todoLists[props.listId].todos
    
    const makeGetVisibleTodos = () => {
      return createSelector(
        [ getVisibilityFilter, getTodos ],
        (visibilityFilter, todos) => {
          switch (visibilityFilter) {
            case 'SHOW_COMPLETED':
              return todos.filter(todo => todo.completed)
            case 'SHOW_ACTIVE':
              return todos.filter(todo => !todo.completed)
            default:
              return todos
          }
        }
      )
    }
    
    export default makeGetVisibleTodos
    

    同时,我们需要让每个组件取使用它们专属的selector。connect中的mapStateToProps参数可以帮到我们。

    如果connect中的mapStateToProps返回一个函数而不是对象,mapStateToProps会被用于为每个组件创建一个独立的mapStateToProps函数。

    下面的例子中,makeMapStateToProps创建了getVisibleTodosselector,并返回了拥有独立selector的mapStateToProps函数。

    const makeMapStateToProps = () => {
      const getVisibleTodos = makeGetVisibleTodos()
      const mapStateToProps = (state, props) => {
        return {
          todos: getVisibleTodos(state, props)
        }
      }
      return mapStateToProps
    }
    

    如果我们将这个makeMapStateToProps传给connect,每个VisibleTodosList组件会或者独立的mapStateToProps和独立的getVisibleTodosselector。记忆缓存功能现在正确的工作了。

    import { connect } from 'react-redux'
    import { toggleTodo } from '../actions'
    import TodoList from '../components/TodoList'
    import { makeGetVisibleTodos } from '../selectors'
    
    const makeMapStateToProps = () => {
      const getVisibleTodos = makeGetVisibleTodos()
      const mapStateToProps = (state, props) => {
        return {
          todos: getVisibleTodos(state, props)
        }
      }
      return mapStateToProps
    }
    
    const mapDispatchToProps = (dispatch) => {
      return {
        onTodoClick: (id) => {
          dispatch(toggleTodo(id))
        }
      }
    }
    
    const VisibleTodoList = connect(
      makeMapStateToProps,
      mapDispatchToProps
    )(TodoList)
    
    export default VisibleTodoList
    

    相关文章

      网友评论

          本文标题:Reselect

          本文链接:https://www.haomeiwen.com/subject/qatuhxtx.html