牛骨文教育服务平台(让学习变的简单)

计算衍生数据

Reselect 是用来创建可记忆的(Memoized)、可组合的 selector 函数。Reselect selectors 可以用来高效地计算 Redux store 里的衍生数据。

可记忆的 Selectors 初衷

首先访问 Todos 列表示例:

containers/App.js

import React, { Component, PropTypes } from "react";
import { connect } from "react-redux";
import { addTodo, completeTodo, setVisibilityFilter, VisibilityFilters } from "../actions";
import AddTodo from "../components/AddTodo";
import TodoList from "../components/TodoList";
import Footer from "../components/Footer";

class App extends Component {
  render() {
    // 通过 connect() 注入:
    const { dispatch, visibleTodos, visibilityFilter } = this.props;
    return (
      <div>
        <AddTodo
          onAddClick={text =>
            dispatch(addTodo(text))
          } />
        <TodoList
          todos={this.props.visibleTodos}
          onTodoClick={index =>
            dispatch(completeTodo(index))
          } />
        <Footer
          filter={visibilityFilter}
          onFilterChange={nextFilter =>
            dispatch(setVisibilityFilter(nextFilter))
          } />
      </div>
    );
  }
}

App.propTypes = {
  visibleTodos: PropTypes.arrayOf(PropTypes.shape({
    text: PropTypes.string.isRequired,
    completed: PropTypes.bool.isRequired
  })),
  visibilityFilter: PropTypes.oneOf([
    "SHOW_ALL",
    "SHOW_COMPLETED",
    "SHOW_ACTIVE"
  ]).isRequired
};

function selectTodos(todos, filter) {
  switch (filter) {
  case VisibilityFilters.SHOW_ALL:
    return todos;
  case VisibilityFilters.SHOW_COMPLETED:
    return todos.filter(todo => todo.completed);
  case VisibilityFilters.SHOW_ACTIVE:
    return todos.filter(todo => !todo.completed);
  }
}

function select(state) {
  return {
    visibleTodos: selectTodos(state.todos, state.visibilityFilter),
    visibilityFilter: state.visibilityFilter
  };
}

// 把组件包起来,以此来注入 dispatch 和 state
export default connect(select)(App);

上面的示例中,select 调用了 selectTodos 来计算 visibleTodos。运行没问题,但有一个缺点:每当组件更新时都会计算 visibleTodos。如果 state tree 非常大,或者计算量非常大,每次更新都重新计算可能会带来性能问题。Reselect 能帮你省去这些没必要的重新计算。

创建可记忆的 Selector

我们需要一个可记忆的 selector 来替代这个 select,只在 state.todos or state.visibilityFilter 变化时重新计算 visibleTodos,而在其它部分(非相关)变化时不做计算。

Reselect 提供 createSelector 函数来创建可记忆的 selector。createSelector 接收一个 input-selectors 数组和一个转换函数作为参数。如果 state tree 的改变会引起 input-selector 值变化,那么 selector 会调用转换函数,传入 input-selectors 作为参数,并返回结果。如果 input-selectors 的值的前一次的一样,它将会直接返回前一次计算的数据,而不会再调用一次转换函数。

让我们定义一个可记忆的 selector visibleTodosSelector 来替代 select

selectors/TodoSelectors.js

import { createSelector } from "reselect";
import { VisibilityFilters } from "./actions";

function selectTodos(todos, filter) {
  switch (filter) {
  case VisibilityFilters.SHOW_ALL:
    return todos;
  case VisibilityFilters.SHOW_COMPLETED:
    return todos.filter(todo => todo.completed);
  case VisibilityFilters.SHOW_ACTIVE:
    return todos.filter(todo => !todo.completed);
  }
}

const visibilityFilterSelector = (state) => state.visibilityFilter;
const todosSelector = (state) => state.todos;

export const visibleTodosSelector = createSelector(
  [visibilityFilterSelector, todosSelector],
  (visibilityFilter, todos) => {
    return {
      visibleTodos: selectTodos(todos, visibilityFilter),
      visibilityFilter
    };
  }
);

在上例中,visibilityFilterSelectortodosSelector 是 input-selector。因为他们并不转换数据,所以被创建成普通的非记忆的 selector 函数。但是,visibleTodosSelector 是一个可记忆的 selector。他接收 visibilityFilterSelectortodosSelector 为 input-selector,还有一个转换函数来计算过滤的 todos 列表。

组合 Selector

可记忆的 selector 自身可以作为其它可记忆的 selector 的 input-selector。下面的 visibleTodosSelector 被当作另一个 selector 的 input-selector,来进一步通过关键字(keyword)过滤 todos。

const keywordSelector = (state) => state.keyword;

const keywordFilterSelector = createSelector(
  [visibleTodosSelector, keywordSelector],
  (visibleTodos, keyword) => visibleTodos.filter(
    todo => todo.indexOf(keyword) > -1
  )
);

连接 Selector 和 Redux Store

如果你在使用 react-redux,你可以使用 connect 来连接可忘记的 selector 和 Redux store。

containers/App.js

import React, { Component, PropTypes } from "react";
import { connect } from "react-redux";
import { addTodo, completeTodo, setVisibilityFilter } from "../actions";
import AddTodo from "../components/AddTodo";
import TodoList from "../components/TodoList";
import Footer from "../components/Footer";
import { visibleTodosSelector } from "../selectors/todoSelectors.js";

class App extends Component {
  render() {
    // 通过 connect() 注入:
    const { dispatch, visibleTodos, visibilityFilter } = this.props;
    return (
      <div>
        <AddTodo
          onAddClick={text =>
            dispatch(addTodo(text))
          } />
        <TodoList
          todos={this.props.visibleTodos}
          onTodoClick={index =>
            dispatch(completeTodo(index))
          } />
        <Footer
          filter={visibilityFilter}
          onFilterChange={nextFilter =>
            dispatch(setVisibilityFilter(nextFilter))
          } />
      </div>
    );
  }
}

App.propTypes = {
  visibleTodos: PropTypes.arrayOf(PropTypes.shape({
    text: PropTypes.string.isRequired,
    completed: PropTypes.bool.isRequired
  })),
  visibilityFilter: PropTypes.oneOf([
    "SHOW_ALL",
    "SHOW_COMPLETED",
    "SHOW_ACTIVE"
  ]).isRequired
};

// 把 selector 传递给连接的组件
export default connect(visibleTodosSelector)(App);