• 计算衍生数据
    • 可记忆的 Selectors 初衷
      • containers/VisibleTodoList.js
    • 创建可记忆的 Selector
      • selectors/index.js
    • 组合 Selector
    • 连接 Selector 和 Redux Store
      • containers/VisibleTodoList.js
    • 在 selectors 中访问 React Props
      • components/App.js
      • selectors/todoSelectors.js
      • containers/VisibleTodoList.js
    • 跨多组件的共享 Selector
      • selectors/todoSelectors.js
      • container/VisibleTodosList.js
    • 下一步

    计算衍生数据

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

    可记忆的 Selectors 初衷

    首先访问 Todos 列表示例:

    containers/VisibleTodoList.js

    1. import { connect } from 'react-redux'
    2. import { toggleTodo } from '../actions'
    3. import TodoList from '../components/TodoList'
    4. const getVisibleTodos = (todos, filter) => {
    5. switch (filter) {
    6. case 'SHOW_ALL':
    7. return todos
    8. case 'SHOW_COMPLETED':
    9. return todos.filter(t => t.completed)
    10. case 'SHOW_ACTIVE':
    11. return todos.filter(t => !t.completed)
    12. }
    13. }
    14. const mapStateToProps = (state) => {
    15. return {
    16. todos: getVisibleTodos(state.todos, state.visibilityFilter)
    17. }
    18. }
    19. const mapDispatchToProps = (dispatch) => {
    20. return {
    21. onTodoClick: (id) => {
    22. dispatch(toggleTodo(id))
    23. }
    24. }
    25. }
    26. const VisibleTodoList = connect(
    27. mapStateToProps,
    28. mapDispatchToProps
    29. )(TodoList)
    30. export default VisibleTodoList

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

    创建可记忆的 Selector

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

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

    定义一个可记忆的 selector getVisibleTodos 来替代上面的无记忆版本:

    selectors/index.js

    1. import { createSelector } from 'reselect'
    2. const getVisibilityFilter = (state) => state.visibilityFilter
    3. const getTodos = (state) => state.todos
    4. export const getVisibleTodos = createSelector(
    5. [ getVisibilityFilter, getTodos ],
    6. (visibilityFilter, todos) => {
    7. switch (visibilityFilter) {
    8. case 'SHOW_ALL':
    9. return todos
    10. case 'SHOW_COMPLETED':
    11. return todos.filter(t => t.completed)
    12. case 'SHOW_ACTIVE':
    13. return todos.filter(t => !t.completed)
    14. }
    15. }
    16. )

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

    组合 Selector

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

    1. const getKeyword = (state) => state.keyword
    2. const getVisibleTodosFilteredByKeyword = createSelector(
    3. [ getVisibleTodos, getKeyword ],
    4. (visibleTodos, keyword) => visibleTodos.filter(
    5. todo => todo.text.indexOf(keyword) > -1
    6. )
    7. )

    连接 Selector 和 Redux Store

    如果你在使用 React Redux,你可以在 mapStateToProps() 中当正常函数来调用 selectors

    containers/VisibleTodoList.js

    1. import { connect } from 'react-redux'
    2. import { toggleTodo } from '../actions'
    3. import TodoList from '../components/TodoList'
    4. import { getVisibleTodos } from '../selectors'
    5. const mapStateToProps = (state) => {
    6. return {
    7. todos: getVisibleTodos(state)
    8. }
    9. }
    10. const mapDispatchToProps = (dispatch) => {
    11. return {
    12. onTodoClick: (id) => {
    13. dispatch(toggleTodo(id))
    14. }
    15. }
    16. }
    17. const VisibleTodoList = connect(
    18. mapStateToProps,
    19. mapDispatchToProps
    20. )(TodoList)
    21. export default VisibleTodoList

    在 selectors 中访问 React Props

    现在假使我们要支持一个新功能:支持多个 Todo 列表新功能。为了简洁起见,省略了实现这个工程会遇到的与本节不相关的内容(reducers 的变化、组件、Actions 等)

    到目前为止,我们只看到 selector 接收 Redux store state 作为参数,然而,selector 也可以接收 props。

    这儿有一个 App 的组件,它渲染了三个叫做 VisibleTodoList 的子组件,每个组件都带一个 listId 的 prop;

    components/App.js

    1. import React from 'react'
    2. import Footer from './Footer'
    3. import AddTodo from '../containers/AddTodo'
    4. import VisibleTodoList from '../containers/VisibleTodoList'
    5. const App = () => (
    6. <div>
    7. <VisibleTodoList listId="1" />
    8. <VisibleTodoList listId="2" />
    9. <VisibleTodoList listId="3" />
    10. </div>
    11. )

    每个 VisibleTodoList 容器根据 listId props 的值选择不同的 state 切片,让我们修改 getVisibilityFiltergetTodos 来接收 props。

    selectors/todoSelectors.js

    1. import { createSelector } from 'reselect'
    2. const getVisibilityFilter = (state, props) =>
    3. state.todoLists[props.listId].visibilityFilter
    4. const getTodos = (state, props) =>
    5. state.todoLists[props.listId].todos
    6. const getVisibleTodos = createSelector(
    7. [ getVisibilityFilter, getTodos ],
    8. (visibilityFilter, todos) => {
    9. switch (visibilityFilter) {
    10. case 'SHOW_COMPLETED':
    11. return todos.filter(todo => todo.completed)
    12. case 'SHOW_ACTIVE':
    13. return todos.filter(todo => !todo.completed)
    14. default:
    15. return todos
    16. }
    17. }
    18. )
    19. export default getVisibleTodos

    props 可以通过 mapStateToProps 传递给 getVisibleTodos:

    1. const mapStateToProps = (state, props) => {
    2. return {
    3. todos: getVisibleTodos(state, props)
    4. }
    5. }

    现在,getVisibleTodos 可以访问 props,一切看上去都是如此的美好。

    但是这儿有一个问题!

    使用带有多个 visibleTodoList 容器实例的 getVisibleTodos selector 不能使用函数记忆功能。

    containers/VisibleTodoList.js

    1. import { connect } from 'react-redux'
    2. import { toggleTodo } from '../actions'
    3. import TodoList from '../components/TodoList'
    4. import { getVisibleTodos } from '../selectors'
    5. const mapStateToProps = (state, props) => {
    6. return {
    7. // 警告:下面的 selector 不会正确记忆
    8. todos: getVisibleTodos(state, props)
    9. }
    10. }
    11. const mapDispatchToProps = (dispatch) => {
    12. return {
    13. onTodoClick: (id) => {
    14. dispatch(toggleTodo(id))
    15. }
    16. }
    17. }
    18. const VisibleTodoList = connect(
    19. mapStateToProps,
    20. mapDispatchToProps
    21. )(TodoList)
    22. export default VisibleTodoList

    createSelector 创建的 selector 只有在参数集与之前的参数集相同时才会返回缓存的值。如果我们交替的渲染 VisibleTodoList listId="1" />VisibleTodoList listId="2" />,共享的 selector 将交替的接收 listId: 1listId: 2。这会导致每次调用时传入的参数不同,因此 selector 将始终重新计算而不是返回缓存的值。我们将在下一节了解如何解决这个限制。

    跨多组件的共享 Selector

    这节中的例子需要 React Redux v4.3.0 或者更高的版本

    为了跨越多个 VisibleTodoList 组件共享 selector,于此同时正确记忆。每个组件的实例需要有拷贝 selector 的私有版本。

    我们创建一个 makeGetVisibleTodos 的函数,在每个调用的时候返回一个 getVisibleTodos selector 的新拷贝。

    selectors/todoSelectors.js

    1. import { createSelector } from 'reselect'
    2. const getVisibilityFilter = (state, props) =>
    3. state.todoLists[props.listId].visibilityFilter
    4. const getTodos = (state, props) =>
    5. state.todoLists[props.listId].todos
    6. const makeGetVisibleTodos = () => {
    7. return createSelector(
    8. [ getVisibilityFilter, getTodos ],
    9. (visibilityFilter, todos) => {
    10. switch (visibilityFilter) {
    11. case 'SHOW_COMPLETED':
    12. return todos.filter(todo => todo.completed)
    13. case 'SHOW_ACTIVE':
    14. return todos.filter(todo => !todo.completed)
    15. default:
    16. return todos
    17. }
    18. }
    19. )
    20. }

    我们还需要一种每个容器访问自己私有 selector 的方式。connectmapStateToProps 函数可以帮助我们。

    如果 connectmapStateToProps 返回的不是一个对象而是一个函数,他将被用做为每个容器的实例创建一个单独的 mapStateToProps 函数。

    下面例子中的 makeMapStateToProps 创建一个新的 getVisibleTodos selectors,返回一个独占新 selector 的权限的 mapStateToProps 函数。

    1. const makeMapStateToProps = () => {
    2. const getVisibleTodos = makeGetVisibleTodos()
    3. const mapStateToProps = (state, props) => {
    4. return {
    5. todos: getVisibleTodos(state, props)
    6. }
    7. }
    8. return mapStateToProps
    9. }

    如果我们通过 makeMapStateToPropsconnectVisibleTodosList 容器的每个组件都会拥有含私有 getVisibleTodos selector 的 mapStateToProps。不论 VisibleTodosList 容器的展现顺序如何,记忆功能都会正常工作。

    container/VisibleTodosList.js

    1. import { connect } from 'react-redux'
    2. import { toggleTodo } from '../actions'
    3. import TodoList from '../components/TodoList'
    4. import { makeGetVisibleTodos } from '../selectors'
    5. const makeMapStateToProps = () => {
    6. const getVisibleTodos = makeGetVisibleTodos()
    7. const mapStateToProps = (state, props) => {
    8. return {
    9. todos: getVisibleTodos(state, props)
    10. }
    11. }
    12. return mapStateToProps
    13. }
    14. const mapDispatchToProps = (dispatch) => {
    15. return {
    16. onTodoClick: (id) => {
    17. dispatch(toggleTodo(id))
    18. }
    19. }
    20. }
    21. const VisibleTodoList = connect(
    22. makeMapStateToProps,
    23. mapDispatchToProps
    24. )(TodoList)
    25. export default VisibleTodoList

    下一步

    查看 官方文档 和 FAQ。当因为太多的衍生计算和重复渲染导致出现性能问题时,大多数的 Redux 项目会开始使用 Reselect。所以在你创建一个大型项目的时候确保你对 reselect 是熟悉的。你也可以去研究他的 源码,这样你就不认为他是黑魔法了。