翻譯|Redux的中間件-Reselect

本文是翻譯Redux的一個中間件文檔.Redux是React的一個數(shù)據(jù)層,React組件的state有關(guān)邏輯處理都被單獨(dú)放到Redux中來進(jìn)行,在state的操作流程中衍生了很多中間件,Reselect這個中間件要解決的問題是:`在組件交互操作的時候,state發(fā)生變化的時候如何減少渲染的壓力.在Reselect中間中使用了緩存機(jī)制,這個機(jī)制可以在javascript的模式設(shè)計(jì)中剛看到介紹,這里就不詳細(xì)說了.僅供參考,以原文為準(zhǔn).


一旦redux從react的數(shù)據(jù)層來理解署海,很多問題都似乎找到了理論依據(jù),所謂名正言順。在web框架中都會用數(shù)據(jù)庫做數(shù)據(jù)持久層媚送,在查表的時候會為了效率做緩存舅逸,reselect是同樣的目的奏瞬。React的組件有自己的特殊性洽议,遇到特殊的特性的時候需要有特殊的處理
方法.

以下是譯文內(nèi)容,原文請參見


“selector”是一個簡單的Redux庫,靈感來源于NuclearJS.

  • Selector可以計(jì)算衍生的數(shù)據(jù),可以讓Redux做到存儲盡可能少的state荐绝。
  • Selector比較高效,只有在某個參數(shù)發(fā)生變化的時候才發(fā)生計(jì)算過程.
  • Selector是可以組合的,他們可以作為輸入,傳遞到其他的selector.
//這個例子不必太在意,后面會有詳細(xì)的介紹
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

實(shí)例

緩存Selcectos的動機(jī)

實(shí)例是基于 Redux Todos List example.

containers/VisibleTodoList.js

import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'

//下面這段代碼是根據(jù)過濾器的state來改變?nèi)粘蘳tate的函數(shù)
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是根據(jù)過濾函數(shù)返回的state低匙,傳入兩個實(shí)參
    todos: getVisibleTodos(state.todos, state.visibilityFilter)
  }
}
//mapDispatchToProps來傳遞dispatch的方法
const mapDispatchToProps = (dispatch) => {
  return {
    onTodoClick: (id) => {
      dispatch(toggleTodo(id))
    }
  }
}
//使用Redux的connect函數(shù)注入state,到TodoList組件
const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

export default VisibleTodoList

在上面的例子中,mapStateToProps調(diào)用getVisibleTodos去計(jì)算todos.這個函數(shù)設(shè)計(jì)的是相當(dāng)好的,但是有個缺點(diǎn):todos在每一次組件更新的時候都會重新計(jì)算.如果state樹的結(jié)構(gòu)比較大,或者計(jì)算比較昂貴,每一次組件更新的時候都進(jìn)行計(jì)算的話,將會導(dǎo)致性能問題.Reselect能夠幫助redux來避免不必要的重新計(jì)算過程.

創(chuàng)建一個緩存Selector

我們可以使用記憶緩存selector代替getVisibleTodos,如果state.todosstate.visibilityFilter發(fā)生變化,他會重新計(jì)算state,但是發(fā)生在其他部分的state變化,就不會重新計(jì)算.

Reslect提供一個函數(shù)createSelector來創(chuàng)建一個記憶selectors.createSelector接受一個input-selectors和一個變換函數(shù)作為參數(shù).如果Redux的state發(fā)生改變造成input-selector的值發(fā)生改變,selector會調(diào)用變換函數(shù),依據(jù)input-selector做參數(shù),返回一個結(jié)果.如果input-selector返回的結(jié)果和前面的一樣,那么就會直接返回有關(guān)state,會省略變換函數(shù)的調(diào)用.

下面我們定義一個記憶selectorgetVisibleTodos替代非記憶的版本

selectors/index.js

import { createSelector } from 'reselect'

const getVisibilityFilter = (state) => state.visibilityFilter
const getTodos = (state) => state.todos
//下面的函數(shù)是經(jīng)過包裝的
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)
    }
  }
)

上面的的實(shí)例中,getVisibilityfiltergetTodos是input-selectors.這兩個函數(shù)是普通的非記憶selector函數(shù),因?yàn)樗麄儧]有變換他們select的數(shù)據(jù).getVisibleTodos另一方面是一個記憶selector.他接收getVisibilityfiltergetTodos作為input-selectors,并且作為一個變換函數(shù)計(jì)算篩選的todo list.

聚合selectors

一個記憶性selector本身也可以作為另一個記憶性selector的input-selector.這里getVisibleTodos可以作為input-selector作為關(guān)鍵字篩選的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

這一部分我們假設(shè)程序?qū)幸粋€擴(kuò)展,我們允許selector支持多todo List.請注意如果要完全實(shí)施這個擴(kuò)展,reducers,components,actions等等都需要作出改變.這些內(nèi)容和主題不是太相關(guān),所以這里就省略掉了.

目前為止,我們僅僅看到selectors接收store的state作為一個參數(shù),其實(shí)一個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應(yīng)該根據(jù)各自的listId屬性獲取state的不同部分.所以我們修改一下getVisibilityFiltergetTodos,便于接受一個屬性參數(shù)

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 //這里是為二維數(shù)組了

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

現(xiàn)在getVisibleTodos可以獲取props,每一部分似乎都工作的不錯.

**但是還有個問題
當(dāng)getVisibleTodosselector和VisibleTodoListcontainer的多個實(shí)例一起工作的時候,記憶功能就不能正常的運(yùn)行:

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創(chuàng)建的selector時候,如果他的參數(shù)集合和上一次的參數(shù)機(jī)會是一樣的,僅僅返回緩存的值.如果我們交替渲染<VisibleTodoList listId="1" /><VisibleTodoList listId="2" />時,共享的selector將會交替接受{listId:1}{listId:2}作為他的props的參數(shù).這將會導(dǎo)致每一次調(diào)用的時候的參數(shù)都不同,因此selector每次都會重新來計(jì)算而不是返回緩存的值.下一部分我們將會介紹怎么解決這個問題.

跨越多個組件使用selectors共性props

這一部分的實(shí)例需要React Redux v4.3.0或者更高版本的支持.

在多個VisibleTodoList組件中共享selector,同時還要保持記憶性,每一個組件的實(shí)例需要他們自己的selector備份.

現(xiàn)在讓我們創(chuàng)建一個函數(shù)makeGetVisibleTodos,這個函數(shù)每次調(diào)用的時候返回一個新的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

我們也需要設(shè)置給每一個組件的實(shí)例他們各自獲取私有的selector方法.mapStateToPropsconnect函數(shù)可以幫助完成這個功能.

**如果mapStateToProps提供給connect不返回一個對象而是一個函數(shù),他就可以被用來為每個組件container創(chuàng)建一個私有的mapStateProps函數(shù).

在下面的實(shí)例中,mapStateProps創(chuàng)建一個新的getVisibleTodosselector,他返回一個mapStateToProps函數(shù),這個函數(shù)能夠接入新的selector.

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

如果我們把makeMapStateToprops傳遞到connect,每一個visibleTodoListcontainer將會獲得各自的含有私有getVisibleTodosselector的mapStateToProps的函數(shù).這樣一來記憶就正常了,不管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數(shù)組,計(jì)算他們的值并且作為參數(shù)傳遞給resultFunc.

createSelector通過判斷input-selector之前調(diào)用和之后調(diào)用的返回值的全等于(===,這個地方英文文獻(xiàn)叫reference equality,引用等于,這個單詞是本質(zhì),中文沒有翻譯出來).經(jīng)過createSelector創(chuàng)建的selector應(yīng)該是immutable(不變的).

經(jīng)過createSelector創(chuàng)建的Selectors有一個緩存,大小是1.這意味著當(dāng)一個input-selector變化的時候,他們總是會重新計(jì)算state,因?yàn)镾elector僅僅存儲每一個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數(shù)組
const totalSelector = createSelector(
  [
    state => state.values.value1,
    state => state.values.value2
  ],
  (value1, value2) => value1 + value2
)

在selector內(nèi)部獲取一個組件的props非常有用.當(dāng)一個selector通過connect函數(shù)連接到一個組件上,組件的屬性作為第二個參數(shù)傳遞給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傳遞的參數(shù).這是createSelector使用的記憶函數(shù).

defaultMemoize 通過調(diào)用equalityCheck函數(shù)來決定一個參數(shù)是否已經(jīng)發(fā)生改變.因?yàn)?code>defaultMemoize設(shè)計(jì)出來就是和immutable數(shù)據(jù)一起使用,默認(rèn)的equalityCheck使用引用全等于來判斷變化:

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

defaultMemoizecreateSelectorCreator配置equalityCheck函數(shù).

createSelectorCreator(memoize,…memoizeOptions)

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

memoize參數(shù)是一個有記憶功能的函數(shù),來代替defaultMemoize.
…memoizeOption展開的參數(shù)是0或者更多的配置選項(xiàng),這些參數(shù)傳遞給memoizeFunc.selectorsresultFunc作為第一個參數(shù)傳遞給memoize,memoizeOptions作為第二個參數(shù):

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 第三個參數(shù)
  option3 // option3 will be passed as fourth argument to customMemoize   第四個參數(shù)
)

const customSelector = customSelectorCreator(
  input1,
  input2,
  resultFunc // resultFunc will be passed as first argument to customMemoize  作為第一個參數(shù)傳遞給customMomize
)

customSelecotr內(nèi)部滴啊用memoize的函數(shù)的代碼如下:

customMemoize(resultFunc, option1, option2, option3)

下面是幾個可能會用到的createSelectorCreator的實(shí)例:

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函數(shù)來緩存未綁定的緩存.

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函數(shù)可以提升便利性.傳遞到connect的selector裝飾者(這是js設(shè)計(jì)模式的概念,可以參考相關(guān)的書籍)接受他的input-selectors,并且在一個對象內(nèi)映射到一個鍵上.

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構(gòu)建的一個對象
const structuredSelector = createSelector(
   mySelectorA,
   mySelectorB,
   mySelectorC,
   (a, b, c) => ({
     a,
     b,
     c
   })
)

createStructuredSelector接受一個對象,這個對象的屬性是input-selectors,函數(shù)返回一個結(jié)構(gòu)性的selector.這個結(jié)構(gòu)性的selector返回一個對象,對象的鍵和inputSelectors的參數(shù)是相同的,但是使用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 }

結(jié)構(gòu)性的selectors可以是嵌套式的:

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


FAQ

Q:為什么當(dāng)輸入的state發(fā)生改變的時候,selector不重新計(jì)算旷痕?

A:檢查一下你的記憶韓式是不是和你的state更新函數(shù)相兼容(例如:如果你正在使用Redux).例如:使用createSelector創(chuàng)建的selector總是創(chuàng)建一個新的對象,原來期待的是更新一個已經(jīng)存在的對象.createSelector使用(===)檢測輸入是否改變,因此如果改變一個已經(jīng)存在的對象沒有觸發(fā)selector重新計(jì)算的原因是改變一個對象的時候沒有觸發(fā)相關(guān)的檢測.提示:如果你正在使用Redux,改變一個state對象的錯誤可能有.

下面的實(shí)例定義了一個selector可以決定數(shù)組的第一個todo項(xiàng)目是不是已經(jīng)被完成:

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

下面的state更新函數(shù)和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更新函數(shù)和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數(shù)據(jù)的需求,你可以使用createSelectorCreator代替默認(rèn)的記憶函數(shù),并且使用不同的等值檢測函數(shù).請參看這里這里作為參考.

Q:為什么input state沒有改變的時候,selector還是會重新計(jì)算?

A: 檢查一下你的記憶函數(shù)和你你的state更新函數(shù)是不是兼容(如果是使用Redux的時候,看看reducer).例如:使用每一次更新的時候,不管值是不是發(fā)生改變,createSelector創(chuàng)建的selector總是會收到一個新的對象.createSelector函數(shù)使用(===)檢測input的變化,由此可知如果每次都返回一個新對象,表示selector總是在每次更新的時候重新計(jì)算.

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調(diào)用的時候,都會重新計(jì)算,因?yàn)锳rray.filter總是返回一個新對象.但是在大多數(shù)情況下,REMOVE_OLD action都不會改變todo列表,所以重新計(jì)算是不必要的.

import { createSelector } from 'reselect'

const todosSelector = state => state.todos

export const visibleTodosSelector = createSelector(
  todosSelector,
  (todos) => {
    ...
  }
)

你可以通過state更新函數(shù)返回一個新對象來減少不必要的重計(jì)算操作,這個對象執(zhí)行深度等值檢測,只有深度不相同的時候才返回新對象.

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中使用深度檢測方法替代默認(rèn)的equalityCheck函數(shù):

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函數(shù)的更替或者在state更新函數(shù)中做深度檢測并不總是比重計(jì)算的花銷小.如果每次重計(jì)算的花銷總是比較小,可能的原因是Reselect沒有通過connect函數(shù)傳遞mapStateProps單純對象的原因.

Q:沒有Redux的情況下可以使用Reselect嗎?

A:可以.Reselect沒有其他任何的依賴包,因此盡管他設(shè)計(jì)的和Redux比較搭配,但是獨(dú)立使用也是可以的.目前的版本在傳統(tǒng)的Flux APP下使用是比較成功的.

如果你使用createSelector創(chuàng)建的selectors,需要確保他的參數(shù)是immutable的.

這里

Q:怎么才能創(chuàng)建一個接收參數(shù)的selector.

A:Reselect沒有支持創(chuàng)建接收參數(shù)的selectors,但是這里有一些實(shí)現(xiàn)類似函數(shù)功能的建議.

如果參數(shù)不是動態(tài)的,你可以使用工廠函數(shù):

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

總的達(dá)成共識看這里超越 neclear-js是:如果一個selector需要動態(tài)的參數(shù),那么參數(shù)應(yīng)該是store中的state.如果你決定好了在應(yīng)用中使用動態(tài)參數(shù),像下面這樣返回一個記憶函數(shù)是比較合適的:

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:默認(rèn)的記憶函數(shù)不太好,我能用個其他的嗎顽冶?

A: 我認(rèn)為這個記憶韓式工作的還可以,但是如果你需要一個其他的韓式也是可以的.
可以看看這個例子

Q:怎么才能測試一個selector?

A:對于一個給定的input,一個selector總是產(chǎn)出相同的結(jié)果.基于這個原因,做單元測試是非常簡單的.

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更新函數(shù)調(diào)用的時候同時檢測selector的記憶函數(shù)的功能也是非常有用的(例如 使用Redux的時候檢查reducer).每一個selector都有一個recomputations方法返回重新計(jì)算的次數(shù):

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保留了最后一個函數(shù)調(diào)用結(jié)果的引用,這個引用作為.resultFunc.如果你已經(jīng)聚合了其他的selectors,這個函數(shù)引用可以幫助你測試每一個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,這個參數(shù)的意圖是在面對復(fù)雜的selector的時候,需要很多獨(dú)立的測試,你不需要管理復(fù)雜的手工計(jì)算,或者為每一個測試創(chuàng)建”傻瓜”selector.

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

A:creatSelector創(chuàng)建的Selectors應(yīng)該可以和Immutable.js數(shù)據(jù)結(jié)構(gòu)一起完美的工作.
如果你的selector正在重計(jì)算,并且你認(rèn)為state沒有發(fā)生變化,一定要確保知道哪一個Immutable.js更新方法,這個方法只要一更新總是返回新對象.哪一個方法只有集合實(shí)際發(fā)生變化的時候才返回新對象.

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)

如果一個操作導(dǎo)致的selector更新總是返回一個新對象,可能會發(fā)生不必要的重計(jì)算.看這里.這是一個關(guān)于pros的討論,使用深全等于來檢測例如immutable.js來減少不必要的重計(jì)算過程.

Q:可以在多個組件之間共享selector嗎欺抗?

A: 使用createSelector創(chuàng)建的Selector的緩存的大小只有1.這個設(shè)定使得多個組件的實(shí)例之間的參數(shù)不同,跨組件共享selector變得不合適.這里也有幾種辦法來解決這個問題:

  • 使用工程函數(shù)方法,為每一個組件實(shí)例創(chuàng)建一個新的selector.這里有一個內(nèi)建的工廠方法,React Redux v4.3或者更高版本可以使用. 看這里
  • 創(chuàng)建一個緩存尺寸大于1的定制selector.

Q:有TypeScript的類型嗎?

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

Q:怎么構(gòu)建一個柯里化selector?

A:嘗試一些這里助手函數(shù),由MattSPalmer提供

有關(guān)的項(xiàng)目

reselect-map

因?yàn)镽eselect不可能保證緩存你所有的需求,在做非常昂貴的計(jì)算的時候,這個方法比較有用.查看一下reselect-maps readme

reselect-map的優(yōu)化措施僅僅使用在一些小的案例中,如果你不確定是不是需要他,就不要使用它.

License

MIT

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末绞呈,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子间景,更是在濱河造成了極大的恐慌佃声,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件倘要,死亡現(xiàn)場離奇詭異圾亏,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)封拧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進(jìn)店門志鹃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人泽西,你說我怎么就攤上這事曹铃。” “怎么了尝苇?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵铛只,是天一觀的道長埠胖。 經(jīng)常有香客問我,道長淳玩,這世上最難降的妖魔是什么直撤? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮蜕着,結(jié)果婚禮上谋竖,老公的妹妹穿的比我還像新娘。我一直安慰自己承匣,他們只是感情好蓖乘,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著韧骗,像睡著了一般嘉抒。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上袍暴,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天些侍,我揣著相機(jī)與錄音,去河邊找鬼政模。 笑死岗宣,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的淋样。 我是一名探鬼主播耗式,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼趁猴!你這毒婦竟也來了刊咳?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤躲叼,失蹤者是張志新(化名)和其女友劉穎芦缰,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體枫慷,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡让蕾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了或听。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片探孝。...
    茶點(diǎn)故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖誉裆,靈堂內(nèi)的尸體忽然破棺而出顿颅,到底是詐尸還是另有隱情,我是刑警寧澤足丢,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布粱腻,位于F島的核電站庇配,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏绍些。R本人自食惡果不足惜捞慌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望柬批。 院中可真熱鬧啸澡,春花似錦、人聲如沸氮帐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽上沐。三九已至皮服,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間参咙,已是汗流浹背冰更。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留昂勒,地道東北人。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓舟铜,卻偏偏與公主長得像戈盈,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子谆刨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內(nèi)容