Redux的中介軟體-Reselect(快取)
本文是翻譯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
-
- Motivation for Memoized Selectors
- Creating a Memoized Selector
- Composing Selectors
- Connecting a Selector to the Redux Store
- Accessing React Props in Selectors
- Sharing Selectors with Props Across Multiple Components
-
- Why isn't my selector recomputing when the input state changes?
- Why is my selector recomputing when the input state stays the same?
- Can I use Reselect without Redux?
- The default memoization function is no good, can I use a different one?
- How do I test a selector?
- How do I create a selector that takes an argument?
- How do I use Reselect with Immutable.js?
- Can I share a selector across multiple components?
- Are there TypeScript typings?
- How can I make a curried selector?
安裝
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.todos
和state.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)
}
}
)
上面的的例項中,getVisibilityfilter
和getTodos
是input-selectors.這兩個函式是普通的非記憶selector函式,因為他們沒有變換他們select的資料.getVisibleTodos
另一方面是一個記憶selector.他接收getVisibilityfilter
和getTodos
作為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>
)
每一個VisibleTodoList
container應該根據各自的listId
屬性獲取state的不同部分.所以我們修改一下getVisibilityFilter
和getTodos
,便於接受一個屬性引數
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
,每一部分似乎都工作的不錯.
**但是還有個問題
當getVisibleTodos
selector和VisibleTodoList
container的多個例項一起工作的時候,記憶功能就不能正常的執行:
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方法.mapStateToProps
的connect
函式可以幫助完成這個功能.
**如果mapStateToProps
提供給connect
不返回一個物件而是一個函式,他就可以被用來為每個元件container建立一個私有的mapStateProps
函式.
在下面的例項中,mapStateProps
建立一個新的getVisibleTodos
selector,他返回一個mapStateToProps
函式,這個函式能夠接入新的selector.
const makeMapStateToProps = () => {
const getVisibleTodos = makeGetVisibleTodos()
const mapStateToProps = (state, props) => {
return {
todos: getVisibleTodos(state, props)
}
}
return mapStateToProps
}
如果我們把makeMapStateToprops
傳遞到connect
,每一個visibleTodoList
container將會獲得各自的含有私有getVisibleTodos
selector的mapStateToProps
的函式.這樣一來記憶就正常了,不管VisibleTodoList
containers的渲染順序怎麼樣.
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
}
defaultMemoize
和createSelectorCreator
去配置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
作者:smartphp
連結:https://www.jianshu.com/p/6e38c66366cd
來源:簡書