翻译|Redux的中间件-Reselect

作者: smartphp | 来源:发表于2017-03-10 16:23 被阅读5789次

    本文是翻译Redux的一个中间件文档.Redux是React的一个数据层,React组件的state有关逻辑处理都被单独放到Redux中来进行,在state的操作流程中衍生了很多中间件,Reselect这个中间件要解决的问题是:`在组件交互操作的时候,state发生变化的时候如何减少渲染的压力.在Reselect中间中使用了缓存机制,这个机制可以在javascript的模式设计中刚看到介绍,这里就不详细说了.仅供参考,以原文为准.


    一旦redux从react的数据层来理解,很多问题都似乎找到了理论依据,所谓名正言顺。在web框架中都会用数据库做数据持久层,在查表的时候会为了效率做缓存,reselect是同样的目的。React的组件有自己的特殊性,遇到特殊的特性的时候需要有特殊的处理
    方法.

    以下是译文内容,原文请参见


    “selector”是一个简单的Redux库,灵感来源于NuclearJS.

    • Selector可以计算衍生的数据,可以让Redux做到存储尽可能少的state。
    • Selector比较高效,只有在某个参数发生变化的时候才发生计算过程.
    • Selector是可以组合的,他们可以作为输入,传递到其他的selector.
    //这个例子不必太在意,后面会有详细的介绍
    import { createSelector } from 'reselect'
    
    const shopItemsSelector = state => state.shop.items
    const taxPercentSelector = state => state.shop.taxPercent
    
    const subtotalSelector = createSelector(
      shopItemsSelector,
      items => items.reduce((acc, item) => acc + item.value, 0)
    )
    
    const taxSelector = createSelector(
      subtotalSelector,
      taxPercentSelector,
      (subtotal, taxPercent) => subtotal * (taxPercent / 100)
    )
    
    export const totalSelector = createSelector(
      subtotalSelector,
      taxSelector,
      (subtotal, tax) => ({ total: subtotal + tax })
    )
    
    let exampleState = {
      shop: {
        taxPercent: 8,
        items: [
          { name: 'apple', value: 1.20 },
          { name: 'orange', value: 0.95 },
        ]
      }
    }
    
    console.log(subtotalSelector(exampleState)) // 2.15
    console.log(taxSelector(exampleState))      // 0.172
    console.log(totalSelector(exampleState))    // { total: 2.322 }
    

    Table of Contents

    安装

    npm install reselect

    实例

    缓存Selcectos的动机

    实例是基于 Redux Todos List example.

    containers/VisibleTodoList.js

    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

    我们可以使用记忆缓存selector代替getVisibleTodos,如果state.todosstate.visibilityFilter发生变化,他会重新计算state,但是发生在其他部分的state变化,就不会重新计算.

    Reslect提供一个函数createSelector来创建一个记忆selectors.createSelector接受一个input-selectors和一个变换函数作为参数.如果Redux的state发生改变造成input-selector的值发生改变,selector会调用变换函数,依据input-selector做参数,返回一个结果.如果input-selector返回的结果和前面的一样,那么就会直接返回有关state,会省略变换函数的调用.

    下面我们定义一个记忆selectorgetVisibleTodos替代非记忆的版本

    selectors/index.js

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

    上面的的实例中,getVisibilityfiltergetTodos是input-selectors.这两个函数是普通的非记忆selector函数,因为他们没有变换他们select的数据.getVisibleTodos另一方面是一个记忆selector.他接收getVisibilityfiltergetTodos作为input-selectors,并且作为一个变换函数计算筛选的todo list.

    聚合selectors

    一个记忆性selector本身也可以作为另一个记忆性selector的input-selector.这里getVisibleTodos可以作为input-selector作为关键字筛选的input-selector:

    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():

    containers/VisibleTodoList.js

    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
    

    在React Props中接入Selectors

    这一部分我们假设程序将会有一个扩展,我们允许selector支持多todo List.请注意如果要完全实施这个扩展,reducers,components,actions等等都需要作出改变.这些内容和主题不是太相关,所以这里就省略掉了.

    目前为止,我们仅仅看到selectors接收store的state作为一个参数,其实一个selector叶可以接受props.

    这里是一个App组件,渲染出三个VisibleTodoList组件,每一个组件有ListId属性.

    components/App.js

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

    每一个VisibleTodoListcontainer应该根据各自的listId属性获取state的不同部分.所以我们修改一下getVisibilityFiltergetTodos,便于接受一个属性参数

    selectors/todoSelectors.js

    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
    

    props可以从mapStateToProps传递到getVisibleTodos

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

    现在getVisibleTodos可以获取props,每一部分似乎都工作的不错.

    **但是还有个问题
    getVisibleTodosselector和VisibleTodoListcontainer的多个实例一起工作的时候,记忆功能就不能正常的运行:

    containers/VisibleTodoList.js

    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
        //⚠️下面的selector不能正确的记忆
        todos: getVisibleTodos(state, props)
      }
    }
    
    const mapDispatchToProps = (dispatch) => {
      return {
        onTodoClick: (id) => {
          dispatch(toggleTodo(id))
        }
      }
    }
    
    const VisibleTodoList = connect(
      mapStateToProps,
      mapDispatchToProps
    )(TodoList)
    
    export default VisibleTodoList
    

    使用createSelector创建的selector时候,如果他的参数集合和上一次的参数机会是一样的,仅仅返回缓存的值.如果我们交替渲染<VisibleTodoList listId="1" /><VisibleTodoList listId="2" />时,共享的selector将会交替接受{listId:1}{listId:2}作为他的props的参数.这将会导致每一次调用的时候的参数都不同,因此selector每次都会重新来计算而不是返回缓存的值.下一部分我们将会介绍怎么解决这个问题.

    跨越多个组件使用selectors共性props

    这一部分的实例需要React Redux v4.3.0或者更高版本的支持.

    在多个VisibleTodoList组件中共享selector,同时还要保持记忆性,每一个组件的实例需要他们自己的selector备份.

    现在让我们创建一个函数makeGetVisibleTodos,这个函数每次调用的时候返回一个新的getVisibleTodos的拷贝:

    selectors/todoSelectors.js

    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方法.mapStateToPropsconnect函数可以帮助完成这个功能.

    **如果mapStateToProps提供给connect不返回一个对象而是一个函数,他就可以被用来为每个组件container创建一个私有的mapStateProps函数.

    在下面的实例中,mapStateProps创建一个新的getVisibleTodosselector,他返回一个mapStateToProps函数,这个函数能够接入新的selector.

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

    如果我们把makeMapStateToprops传递到connect,每一个visibleTodoListcontainer将会获得各自的含有私有getVisibleTodosselector的mapStateToProps的函数.这样一来记忆就正常了,不管VisibleTodoListcontainers的渲染顺序怎么样.

    containers/VisibleTodoList.js

    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
    

    API

    createSelector(…inputSelectors|[inputSelectors],resultFunc)

    接受一个或者多个selectors,或者一个selectors数组,计算他们的值并且作为参数传递给resultFunc.

    createSelector通过判断input-selector之前调用和之后调用的返回值的全等于(===,这个地方英文文献叫reference equality,引用等于,这个单词是本质,中文没有翻译出来).经过createSelector创建的selector应该是immutable(不变的).

    经过createSelector创建的Selectors有一个缓存,大小是1.这意味着当一个input-selector变化的时候,他们总是会重新计算state,因为Selector仅仅存储每一个input-selector前一个值.

    const mySelector = createSelector(
      state => state.values.value1,
      state => state.values.value2,
      (value1, value2) => value1 + value2
    )
    
    // You can also pass an array of selectors
    //可以出传递一个selector数组
    const totalSelector = createSelector(
      [
        state => state.values.value1,
        state => state.values.value2
      ],
      (value1, value2) => value1 + value2
    )
    

    在selector内部获取一个组件的props非常有用.当一个selector通过connect函数连接到一个组件上,组件的属性作为第二个参数传递给selector:

    const abSelector = (state, props) => state.a * props.b
    
    // props only (ignoring state argument)
    const cSelector =  (_, props) => props.c
    
    // state only (props argument omitted as not required)
    const dSelector = state => state.d
    
    const totalSelector = createSelector(
      abSelector,
      cSelector,
      dSelector,
      (ab, c, d) => ({
        total: ab + c + d
      })
    )
    
    

    defaultMemoize(func, equalityCheck = defaultEqualityCheck)

    defaultMemoize能记住通过func传递的参数.这是createSelector使用的记忆函数.

    defaultMemoize 通过调用equalityCheck函数来决定一个参数是否已经发生改变.因为defaultMemoize设计出来就是和immutable数据一起使用,默认的equalityCheck使用引用全等于来判断变化:

    function defaultEqualityCheck(currentVal, previousVal) {
      return currentVal === previousVal
    }
    

    defaultMemoizecreateSelectorCreator配置equalityCheck函数.

    createSelectorCreator(memoize,…memoizeOptions)

    createSelectorCreator用来配置定制版本的createSelector.

    memoize参数是一个有记忆功能的函数,来代替defaultMemoize.
    …memoizeOption展开的参数是0或者更多的配置选项,这些参数传递给memoizeFunc.selectorsresultFunc作为第一个参数传递给memoize,memoizeOptions作为第二个参数:

    const customSelectorCreator = createSelectorCreator(
      customMemoize, // function to be used to memoize resultFunc,记忆resultFunc
      option1, // option1 will be passed as second argument to customMemoize 第二个惨呼
      option2, // option2 will be passed as third argument to customMemoize 第三个参数
      option3 // option3 will be passed as fourth argument to customMemoize   第四个参数
    )
    
    const customSelector = customSelectorCreator(
      input1,
      input2,
      resultFunc // resultFunc will be passed as first argument to customMemoize  作为第一个参数传递给customMomize
    )
    

    customSelecotr内部滴啊用memoize的函数的代码如下:

    customMemoize(resultFunc, option1, option2, option3)
    

    下面是几个可能会用到的createSelectorCreator的实例:

    defaultMemoize配置equalityCheck

    import { createSelectorCreator, defaultMemoize } from 'reselect'
    import isEqual from 'lodash.isEqual'
    
    // create a "selector creator" that uses lodash.isEqual instead of ===
    const createDeepEqualSelector = createSelectorCreator(
      defaultMemoize,
      isEqual
    )
    
    // use the new "selector creator" to create a selector
    const mySelector = createDeepEqualSelector(
      state => state.values.filter(val => val < 5),
      values => values.reduce((acc, val) => acc + val, 0)
    )
    

    使用loadsh的memoize函数来缓存未绑定的缓存.

    import { createSelectorCreator } from 'reselect'
    import memoize from 'lodash.memoize'
    
    let called = 0
    const hashFn = (...args) => args.reduce(
      (acc, val) => acc + '-' + JSON.stringify(val),
      ''
    )
    const customSelectorCreator = createSelectorCreator(memoize, hashFn)
    const selector = customSelectorCreator(
      state => state.a,
      state => state.b,
      (a, b) => {
        called++
        return a + b
      }
    )
    

    createStructuredSelector({inputSelectors}, selectorCreator = createSelector)

    如果在普通的模式下使用createStructuredSelector函数可以提升便利性.传递到connect的selector装饰者(这是js设计模式的概念,可以参考相关的书籍)接受他的input-selectors,并且在一个对象内映射到一个键上.

    const mySelectorA = state => state.a
    const mySelectorB = state => state.b
    
    // The result function in the following selector
    // is simply building an object from the input selectors 由selectors构建的一个对象
    const structuredSelector = createSelector(
       mySelectorA,
       mySelectorB,
       mySelectorC,
       (a, b, c) => ({
         a,
         b,
         c
       })
    )
    

    createStructuredSelector接受一个对象,这个对象的属性是input-selectors,函数返回一个结构性的selector.这个结构性的selector返回一个对象,对象的键和inputSelectors的参数是相同的,但是使用selectors代替了其中的值.

    const mySelectorA = state => state.a
    const mySelectorB = state => state.b
    
    const structuredSelector = createStructuredSelector({
      x: mySelectorA,
      y: mySelectorB
    })
    
    const result = structuredSelector({ a: 1, b: 2 }) // will produce { x: 1, y: 2 }
    

    结构性的selectors可以是嵌套式的:

    const nestedSelector = createStructuredSelector({
      subA: createStructuredSelector({
        selectorA,
        selectorB
      }),
      subB: createStructuredSelector({
        selectorC,
        selectorD
      })
    })
    
    

    FAQ

    Q:为什么当输入的state发生改变的时候,selector不重新计算?

    A:检查一下你的记忆韩式是不是和你的state更新函数相兼容(例如:如果你正在使用Redux).例如:使用createSelector创建的selector总是创建一个新的对象,原来期待的是更新一个已经存在的对象.createSelector使用(===)检测输入是否改变,因此如果改变一个已经存在的对象没有触发selector重新计算的原因是改变一个对象的时候没有触发相关的检测.提示:如果你正在使用Redux,改变一个state对象的错误可能有.

    下面的实例定义了一个selector可以决定数组的第一个todo项目是不是已经被完成:

    const isFirstTodoCompleteSelector = createSelector(
      state => state.todos[0],
      todo => todo && todo.completed
    )
    

    下面的state更新函数和isFirstTodoCompleteSelector将不会正常工作工作:

    export default function todos(state = initialState, action) {
      switch (action.type) {
      case COMPLETE_ALL:
        const areAllMarked = state.every(todo => todo.completed)
        // BAD: mutating an existing object
        return state.map(todo => {
          todo.completed = !areAllMarked
          return todo
        })
    
      default:
        return state
      }
    }
    

    下面的state更新函数和isFirstTodoComplete一起可以正常工作.

    export default function todos(state = initialState, action) {
      switch (action.type) {
      case COMPLETE_ALL:
        const areAllMarked = state.every(todo => todo.completed)
        // GOOD: returning a new object each time with Object.assign
        return state.map(todo => Object.assign({}, todo, {
          completed: !areAllMarked
        }))
    
      default:
        return state
      }
    }
    

    如果你没有使用Redux,但是有使用mutable数据的需求,你可以使用createSelectorCreator代替默认的记忆函数,并且使用不同的等值检测函数.请参看这里这里作为参考.

    Q:为什么input state没有改变的时候,selector还是会重新计算?

    A: 检查一下你的记忆函数和你你的state更新函数是不是兼容(如果是使用Redux的时候,看看reducer).例如:使用每一次更新的时候,不管值是不是发生改变,createSelector创建的selector总是会收到一个新的对象.createSelector函数使用(===)检测input的变化,由此可知如果每次都返回一个新对象,表示selector总是在每次更新的时候重新计算.

    import { REMOVE_OLD } from '../constants/ActionTypes'
    
    const initialState = [
      {
        text: 'Use Redux',
        completed: false,
        id: 0,
        timestamp: Date.now()
      }
    ]
    
    export default function todos(state = initialState, action) {
      switch (action.type) {
      case REMOVE_OLD:
        return state.filter(todo => {
          return todo.timestamp + 30 * 24 * 60 * 60 * 1000 > Date.now()
        })
      default:
        return state
      }
    }
    

    下面的selector在每一次REMOVE_OLD调用的时候,都会重新计算,因为Array.filter总是返回一个新对象.但是在大多数情况下,REMOVE_OLD action都不会改变todo列表,所以重新计算是不必要的.

    import { createSelector } from 'reselect'
    
    const todosSelector = state => state.todos
    
    export const visibleTodosSelector = createSelector(
      todosSelector,
      (todos) => {
        ...
      }
    )
    

    你可以通过state更新函数返回一个新对象来减少不必要的重计算操作,这个对象执行深度等值检测,只有深度不相同的时候才返回新对象.

    import { REMOVE_OLD } from '../constants/ActionTypes'
    import isEqual from 'lodash.isEqual'
    
    const initialState = [
      {
        text: 'Use Redux',
        completed: false,
        id: 0,
        timestamp: Date.now()
      }
    ]
    
    export default function todos(state = initialState, action) {
      switch (action.type) {
      case REMOVE_OLD:
        const updatedState =  state.filter(todo => {
          return todo.timestamp + 30 * 24 * 60 * 60 * 1000 > Date.now()
        })
        return isEqual(updatedState, state) ? state : updatedState
      default:
        return state
      }
    }
    

    替代的方法是,在selector中使用深度检测方法替代默认的equalityCheck函数:

    import { createSelectorCreator, defaultMemoize } from 'reselect'
    import isEqual from 'lodash.isEqual'
    
    const todosSelector = state => state.todos
    
    // create a "selector creator" that uses lodash.isEqual instead of ===
    const createDeepEqualSelector = createSelectorCreator(
      defaultMemoize,
      isEqual
    )
    
    // use the new "selector creator" to create a selector
    const mySelector = createDeepEqualSelector(
      todosSelector,
      (todos) => {
        ...
      }
    )
    

    检查equalityCheck函数的更替或者在state更新函数中做深度检测并不总是比重计算的花销小.如果每次重计算的花销总是比较小,可能的原因是Reselect没有通过connect函数传递mapStateProps单纯对象的原因.

    Q:没有Redux的情况下可以使用Reselect吗?

    A:可以.Reselect没有其他任何的依赖包,因此尽管他设计的和Redux比较搭配,但是独立使用也是可以的.目前的版本在传统的Flux APP下使用是比较成功的.

    如果你使用createSelector创建的selectors,需要确保他的参数是immutable的.

    这里

    Q:怎么才能创建一个接收参数的selector.

    A:Reselect没有支持创建接收参数的selectors,但是这里有一些实现类似函数功能的建议.

    如果参数不是动态的,你可以使用工厂函数:

    const expensiveItemSelectorFactory = minValue => {
      return createSelector(
        shopItemsSelector,
        items => items.filter(item => item.value > minValue)
      )
    }
    
    const subtotalSelector = createSelector(
      expensiveItemSelectorFactory(200),
      items => items.reduce((acc, item) => acc + item.value, 0)
    )
    

    总的达成共识看这里超越 neclear-js是:如果一个selector需要动态的参数,那么参数应该是store中的state.如果你决定好了在应用中使用动态参数,像下面这样返回一个记忆函数是比较合适的:

    import { createSelector } from 'reselect'
    import memoize from 'lodash.memoize'
    
    const expensiveSelector = createSelector(
      state => state.items,
      items => memoize(
        minValue => items.filter(item => item.value > minValue)
      )
    )
    
    const expensiveFilter = expensiveSelector(state)
    
    const slightlyExpensive = expensiveFilter(100)
    const veryExpensive = expensiveFilter(1000000)
    

    Q:默认的记忆函数不太好,我能用个其他的吗?

    A: 我认为这个记忆韩式工作的还可以,但是如果你需要一个其他的韩式也是可以的.
    可以看看这个例子

    Q:怎么才能测试一个selector?

    A:对于一个给定的input,一个selector总是产出相同的结果.基于这个原因,做单元测试是非常简单的.

    const selector = createSelector(
      state => state.a,
      state => state.b,
      (a, b) => ({
        c: a * 2,
        d: b * 3
      })
    )
    
    test("selector unit test", () => {
      assert.deepEqual(selector({ a: 1, b: 2 }), { c: 2, d: 6 })
      assert.deepEqual(selector({ a: 2, b: 3 }), { c: 4, d: 9 })
    })
    

    在state更新函数调用的时候同时检测selector的记忆函数的功能也是非常有用的(例如 使用Redux的时候检查reducer).每一个selector都有一个recomputations方法返回重新计算的次数:

    suite('selector', () => {
      let state = { a: 1, b: 2 }
    
      const reducer = (state, action) => (
        {
          a: action(state.a),
          b: action(state.b)
        }
      )
    
      const selector = createSelector(
        state => state.a,
        state => state.b,
        (a, b) => ({
          c: a * 2,
          d: b * 3
        })
      )
    
      const plusOne = x => x + 1
      const id = x => x
    
      test("selector unit test", () => {
        state = reducer(state, plusOne)
        assert.deepEqual(selector(state), { c: 4, d: 9 })
        state = reducer(state, id)
        assert.deepEqual(selector(state), { c: 4, d: 9 })
        assert.equal(selector.recomputations(), 1)
        state = reducer(state, plusOne)
        assert.deepEqual(selector(state), { c: 6, d: 12 })
        assert.equal(selector.recomputations(), 2)
      })
    })
    

    另外,selectors保留了最后一个函数调用结果的引用,这个引用作为.resultFunc.如果你已经聚合了其他的selectors,这个函数引用可以帮助你测试每一个selector,不需要从state中解耦测试.

    例如如果你的selectors集合像下面这样:
    selectors.js

    export const firstSelector = createSelector( ... )
    export const secondSelector = createSelector( ... )
    export const thirdSelector = createSelector( ... )
    
    export const myComposedSelector = createSelector(
      firstSelector,
      secondSelector,
      thirdSelector,
      (first, second, third) => first * second < third
    )
    

    单元测试就像下面这样:
    test/selectors.js

    // tests for the first three selectors...
    test("firstSelector unit test", () => { ... })
    test("secondSelector unit test", () => { ... })
    test("thirdSelector unit test", () => { ... })
    
    // We have already tested the previous
    // three selector outputs so we can just call `.resultFunc`
    // with the values we want to test directly:
    test("myComposedSelector unit test", () => {
      // here instead of calling selector()
      // we just call selector.resultFunc()
      assert(selector.resultFunc(1, 2, 3), true)
      assert(selector.resultFunc(2, 2, 1), false)
    })
    

    最后,每一个selector有一个resetRecomputations方法,重置recomputations方法为0,这个参数的意图是在面对复杂的selector的时候,需要很多独立的测试,你不需要管理复杂的手工计算,或者为每一个测试创建”傻瓜”selector.

    Q:Reselect怎么和Immutble.js一起使用?

    A:creatSelector创建的Selectors应该可以和Immutable.js数据结构一起完美的工作.
    如果你的selector正在重计算,并且你认为state没有发生变化,一定要确保知道哪一个Immutable.js更新方法,这个方法只要一更新总是返回新对象.哪一个方法只有集合实际发生变化的时候才返回新对象.

    import Immutable from 'immutable'
    
    let myMap = Immutable.Map({
      a: 1,
      b: 2,
      c: 3
    })
    
     // set, merge and others only return a new obj when update changes collection
    let newMap = myMap.set('a', 1)
    assert.equal(myMap, newMap)
    newMap = myMap.merge({ 'a', 1 })
    assert.equal(myMap, newMap)
    // map, reduce, filter and others always return a new obj
    newMap = myMap.map(a => a * 1)
    assert.notEqual(myMap, newMap)
    

    如果一个操作导致的selector更新总是返回一个新对象,可能会发生不必要的重计算.看这里.这是一个关于pros的讨论,使用深全等于来检测例如immutable.js来减少不必要的重计算过程.

    Q:可以在多个组件之间共享selector吗?

    A: 使用createSelector创建的Selector的缓存的大小只有1.这个设定使得多个组件的实例之间的参数不同,跨组件共享selector变得不合适.这里也有几种办法来解决这个问题:

    • 使用工程函数方法,为每一个组件实例创建一个新的selector.这里有一个内建的工厂方法,React Redux v4.3或者更高版本可以使用. 看这里
    • 创建一个缓存尺寸大于1的定制selector.

    Q:有TypeScript的类型吗?

    A: 是的!他们包含在package.json里.可以很好的工作.

    Q:怎么构建一个柯里化selector?

    A:尝试一些这里助手函数,由MattSPalmer提供

    有关的项目

    reselect-map

    因为Reselect不可能保证缓存你所有的需求,在做非常昂贵的计算的时候,这个方法比较有用.查看一下reselect-maps readme

    reselect-map的优化措施仅仅使用在一些小的案例中,如果你不确定是不是需要他,就不要使用它.

    License

    MIT

    相关文章

      网友评论

      • 小球球的宋先生::anguished: 感觉像是作者肢解粘的谷歌翻译,很多地方语句不通顺,像是机器翻译
      • 0eae732b7e2a:好难弄。。
      • smartphp:一旦redux从react的数据层来理解,很多问题都似乎找到了理论依据,所谓名正言顺了。在web框架中都会用数据库做数据持久层,在查表的时候会为了效率做缓存,reselect是同样的目的。

      本文标题:翻译|Redux的中间件-Reselect

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