yuxino / blog

🎬 Life's a Movie
17 stars 2 forks source link

reselect - selector library for redux #42

Closed yuxino closed 3 years ago

yuxino commented 6 years ago

reselect的灵感来自NuclearJS的getter部分。re-frame的subscriptions和speedskater提议

我们看看一个Selector可以做什么 ? 一下是官方提到的三个重点。

Selectors can compute derived data, allowing Redux to store the minimal possible state. Selectors are efficient. A selector is not recomputed unless one of its arguments changes. Selectors are composable. They can be used as input to other selectors.

翻译过来大概就是

如果对文字不感兴趣也可以直接看视频教程。视频讲解了文字部分的所有内容。

上面的说法其实非常抽象。让我们看一个例子

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 }

这个例子是计算商品的总税务。如果无法理解的话也不要紧。我们先看看createSelector的函数体和官方的描述。

createSelector(...inputSelectors | [inputSelectors], resultFunc)

Takes one or more selectors, or an array of selectors, computes their values and passes them as arguments to resultFunc.

createSelector可以接受一个或者一组的inputSelector或者一个inputSelectors作为参数。参数的最后一个总会是一个resultFunc。resultFunc会接收之前的inputSelector参数计算出来的值作为参数。resultFunc是一个回调函数。

使用方法如下

const mySelector = createSelector(
  state => state.values.value1,
  state => state.values.value2,
  (value1, value2) => value1 + value2
)

// You can also pass an array of selectors
const totalSelector = createSelector(
  [
    state => state.values.value1,
    state => state.values.value2
  ],
  (value1, value2) => value1 + value2
)

其他还有更高级的说明在后面写下来。

我们现在回头看最早之前的那个demo。现在已经非常明了。createSelector接受inputSelector返回新的inputSelector。感觉就像这样。

createSelector(...inputSelectors | [inputSelectors], resultFunc) : inputSelector

所以demo里的所有方法变量都是Selector结尾。最前面的两个Selector是取state里面的状态的,是之后用来做参数用的。后面的这一段调用了createSelector方法。

const subtotalSelector = createSelector(
  shopItemsSelector,
  items => items.reduce((acc, item) => acc + item.value, 0)
)

使用Es6的reduce计算了商品的总价值。因为createSelector允许多个Selector作为参数。所以第二步做了这个。

const taxSelector = createSelector(
  subtotalSelector,
  taxPercentSelector,
  (subtotal, taxPercent) => subtotal * (taxPercent / 100)
)

利用了taxPercentSelector和之前定义的subtotalSelector计算了一波总税务。

最后把商品总价和商品总税务合并起来 算出税后商品价格。整个过程非常清晰明确。

export const totalSelector = createSelector(
  subtotalSelector,
  taxSelector,
  (subtotal, tax) => ({ total: subtotal + tax })
)

emmmm 虽然很清晰明确 但是也并非是我们利用他的理由。利用reselect并不是为了这样,利用reselect的真正原因会在下面说到。

接下来这里记录怎么用Reselect的selector。

但是你想知道为什么需要reselect,得看这里

reselect提供的是带有记忆(缓存)能力的Selector。为了展示这一点。我们会用一个最基本的todo例子说明。这个例子取自基于最基础的redux todo list example

containers/VisibleTodoList.js

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

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: getVisibleTodos(state.todos, state.visibilityFilter)
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    onTodoClick: (id) => {
      dispatch(toggleTodo(id))
    }
  }
}

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

export default VisibleTodoList

In the above example, mapStateToProps calls getVisibleTodos to calculate todos. This works great, but there is a drawback: todos is calculated every time the state tree is updated. If the state tree is large, or the calculation expensive, repeating the calculation on every update may cause performance problems. Reselect can help to avoid these unnecessary recalculations.

上面的例子mapStateToProps调用了getVisibleTodos去计算todos。这工作起来非常的棒。但是有缺点:当每次状态树改变的时候todos都会被重新计算(不相关的状态改变也会触发)。如果状态树很庞大或者计算一次的代价非常昂贵。如果是这样每次重新计算会带来性能问题。Reselect可以帮助你避免掉不必要的重计算。

那么接下来我们将会使用Reselect来改造这个简单的todo demo。

We would like to replace getVisibleTodos with a memoized selector that recalculates todos when the value of state.todos or state.visibilityFilter changes, but not when changes occur in other (unrelated) parts of the state tree.

我们要把之前的getVisibleTodos替换成一个有记忆(缓存)能力的Selector这个Selector重计算只会发生在state.todos或者state.visibilityFilter改变的时候。其他不相关的状态改变都不会触发重计算。

Reselect provides a function createSelector for creating memoized selectors. createSelector takes an array of input-selectors and a transform function as its arguments. If the Redux state tree is mutated in a way that causes the value of an input-selector to change, the selector will call its transform function with the values of the input-selectors as arguments and return the result. If the values of the input-selectors are the same as the previous call to the selector, it will return the previously computed value instead of calling the transform function.

Reselect提供了createSelector方法创建一个可记忆(缓存)的Selector。这个方法以一组Selector函数和一个转换函数(前面提到的resultFunc)作为参数。如果Redux状态树通过突变的方式导致input-selector变化了。那么selector将会调用transfrom方法并把input-selector作为参数再返回结果。如果input-selector的和之前的调用用到的input-selector一样的话。selector将会返回之前计算出来的结果而不是调用transform方法。

Let's define a memoized selector named getVisibleTodos to replace the non-memoized version above:

好了好了 让我们来定义一个可记忆(缓存)的selector并且取名叫做getVisibleTodos去替换之前没有用可记忆(缓存)的版本吧 :

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

是的这个例子和前面的大同小异。只是参数的位置有点不一样其他都大同小异。但是真的运行的时候区别还是蛮大的。

In the example above, getVisibilityFilter and getTodos are input-selectors. They are created as ordinary non-memoized selector functions because they do not transform the data they select. getVisibleTodos on the other hand is a memoized selector. It takes getVisibilityFilter and getTodos as input-selectors, and a transform function that calculates the filtered todos list.

在上面的例子中。getVisibilityFiltergetTodos都算是input-selector。最后一个参数是一个箭头函数。这个参数被认为是transfrom方法。Reselect会在状态改变的时候调用它过滤todo列表。

yuxino commented 6 years ago

失去梦想不想更新了。直接看文档吧。guna