從新翻譯版本|Redux-Reselect 文檔

「selector」是一個簡單的Redux庫,靈感來源於NuclearJS.html

  • 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 reselectredux

實例

緩存Selcectos的動機

實例是基於 Redux Todos List example.設計模式

containers/VisibleTodoList.jsapi

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

相關文章
相關標籤/搜索