Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

重新翻译版本|Redux-Reselect 文档 #27

Open
eightHundreds opened this issue Mar 29, 2021 · 0 comments
Open

重新翻译版本|Redux-Reselect 文档 #27

eightHundreds opened this issue Mar 29, 2021 · 0 comments

Comments

@eightHundreds
Copy link
Owner

“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, 你可以 直接在mapStateToProps()中调用 selector:

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
复制代码

在 Selectors 中获取 React 的 props

这一部分我们假设程序将会有一个扩展, 我们允许 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中就会创建独立的mapStateToProps实例.

在下面的实例中,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
https://juejin.cn/post/6844903829381595150

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant