React学习笔记——使用Redux进行状态管理

1,094 阅读7分钟

Redux是什么

首先要分清楚几个容易混淆的概念Reduxreact-reduxFlux

Flux和Redux

FluxRedux是同一个层级的概念,他们都是用于React状态管理的一套架构模式。ReduxFlux的演进,他们有如下的共同点。

  1. 单向数据流,即数据只能由store流向view,而不能反过来
  2. 通过Action来更新数据

可以看到,两者的设计理念基本一致,都是通过限制数据更新的过程,让视图的更新更加容易预测。

在此基础上,Redux做了如下改进

  1. 数据源唯一,store全局唯一,状态树共用
  2. 保持状态只读,更新的时候不是直接修改状态,只能通过reducer返回新的状态
  3. 使用纯函数,状态更新的过程不会引起除了被更新状态之外的其他变化,同样的输入一定带来同样的输出,状态的变化是确定的

这种修改,一定程度上降低了多数据源管理的复杂程度,也为回溯组件状态带来可能,但是设计上比Flux要难理解一些

Redux和react-redux

react-redux是在实际React项目中帮助我们使用Redux的插件,就是这么简单。

什么时候使用Redux

先看看Redux的状态管理有何不同,画几个简单的图就能理解了

文章配图.png

以上是抽象出来的传统和Redux简单工作方式。

我们可以看到,在传统方式下,如果是来自更高层级组件的数据源变化,那么数据的流向是非常自然的,通过props很容易能将父组件的数据传递到子组件。但如果数据源变化改变了子组件的状态,这时候想要引起绿色目标组件的状态变化,数据的传播方向就会从子组件到父组件,当数据流转到引起变化的组件和目标组件的共同父组件的时候,又会回到上一种情况。

一般我们会通过把控制父组件状态的函数通过props传入子组件,让子组件调用的方式来实现第二种情况。举个例子,页面将控制SET modelVisibal = false的函数传入Modal框中,绑定到关闭按钮的onClick回调上,实现关闭的作用。

使用Redux的情况下,当数据源变化,会通过容器组件派发任务的形式更新store,数据再从store中流入各个组件。

那到底什么时候使用Redux呢?

在调研学习的时候,这样一句话让我印象深刻

“如果你不知道是否需要Redux,那就是不需要它”

如果使用传统方法能让你hold的住项目的状态管理以及后续的开发维护,OK,Redux对你来说可能不那么必要,反而使用还会加大工作量。但如果你苦恼于过多的兄弟组件传值,能修改视图的数据源太多太复杂,那么Redux可能能救你于水火之中。

一言蔽之,一切框架的使用都是为了减少重复工作,让项目往工程化方向演进。

怎么使用Redux

Redux在项目中的使用需要借助react-redux(内心OS:终于轮到我出场了!),我们借助官方的TodoList项目的例子给大家讲解一下。

  • 第一步,创建一个全局唯一的store并绑定到根节点

    // store.js
    import { createStore } from "redux";
    import rootReducer from "./reducers";
    
    export default createStore(rootReducer);
    
    // index.js
    ...
    import { Provider } from "react-redux";
    ...
    
    const rootElement = document.getElementById("root");
    ReactDOM.render(
      <Provider store={store}>
        <TodoApp />
      </Provider>,
      rootElement
    );
    

    store.js,调用createStore方法创建了一个store并且export出去,我们先略过参数rootReducer,等等再说明。

    接着在index.js中,我们借助react-redux插件中的Provider组件,将我们的业务组件包装,并将刚刚创建的store挂载到上面。

  • 第二步,创建Action,我们以添加addTodo为例

    // action.js
    ...
    export const addTodo = content => ({
      type: ADD_TODO,
      payload: {
        id: ++nextTodoId,
        content
      }
    });
    
    ...
    

    我们创建的addTodo是一个箭头函数,参数content是容器组件触发动作时要传的参数。返回一个会被reducer处理的对象,type是一个常量,用于区分不同的Actionpayloadreducer更新状态的依据。

  • 第三步,创建reducer,处理addTodo

    // reducers/todos.js
    ...
    export default function(state = initialState, action) {
      switch (action.type) {
        case ADD_TODO: {
          const { id, content } = action.payload;
          return {
            ...state,
            allIds: [...state.allIds, id],
            byIds: {
              ...state.byIds,
              [id]: {
                content,
                completed: false
              }
            }
          };
        }
        ...
    }
    ...
    

    reducer是我们自己定义来处理Action的函数,接受的参数有state(如果不传,就用初始化的默认参数)和action

    当判断action的类型是ADD_TODO时,会从payload里取出触发Action时组件传入的参数,返回给store一个最新的状态。

    要注意的是,reducer不会用直接赋值的方式修改store或者state,而是采用return一个确定的新状态来更新store

  • 第四步,使用store中的状态

    import React from "react";
    import { connect } from "react-redux";
    import Todo from "./Todo";
    
    import { getTodosByVisibilityFilter } from "../redux/selectors";
    import { VISIBILITY_FILTERS } from "../constants";
    
    const TodoList = ({ todos }) => (
      <ul className="todo-list">
        {todos && todos.length
          ? todos.map((todo, index) => {
              return <Todo key={`todo-${todo.id}`} todo={todo} />;
            })
          : "No todos, yay!"}
      </ul>
    );
    
    const mapStateToProps = state => {
      const { visibilityFilter } = state;
      const todos = getTodosByVisibilityFilter(state, visibilityFilter);
      return { todos };
    };
    
    // export default TodoList;
    export default connect(mapStateToProps)(TodoList);
    

    同样,我们需要借助connect这个react-redux中的工具人来帮助我们在组件中使用store

    connect函数第一个参数,可以对状态树做过滤,mapStateToProps接受整个状态树,然后过滤出组件需要的状态,返回一个对象,这个对象会作为组件的props传入组件。

    connect函数会返回一个包装函数,我们需要把原来的组件传入其中,组件就能获得store的使用环境。

  • 第五步,触发Action改变状态

    import React from "react";
    import { connect } from "react-redux";
    import { addTodo } from "../redux/actions";
    
    class AddTodo extends React.Component {
      constructor(props) {
        super(props);
        this.state = { input: "" };
      }
    
      updateInput = input => {
        this.setState({ input });
      };
    
      handleAddTodo = () => {
        this.props.addTodo(this.state.input);
        this.setState({ input: "" });
      };
    
      render() {
        return (
          <div>
            <input
              onChange={e => this.updateInput(e.target.value)}
              value={this.state.input}
            />
            <button className="add-todo" onClick={this.handleAddTodo}>
              Add Todo
            </button>
          </div>
        );
      }
    }
    
    export default connect(
      null,
      { addTodo }
    )(AddTodo);
    // export default AddTodo;
    

    要在组件中使用Action,我们双要用到connect这个函数,这个函数接受的第二个参数,是一个装着action的对象,这些action会被包装进组件的props中。我们就可以在组件中调用addTodo这个action。而这些被分发(dispatch)的action会被处理。

    还记得我们第一步略过的rootReducer对象吗,它是这样创建的

    import { combineReducers } from "redux";
    import visibilityFilter from "./visibilityFilter";
    import todos from "./todos";
    
    export default combineReducers({ todos, visibilityFilter });
    

    这个被export出去的就是rootReducer对象,我们把要注册的reducer通过combineReducers方法结合后,注册在store中,他们就能自动拦截action,完成数据的处理啦。

小结和一些感想

小结

本文对Redux做了一个简单的介绍,受限于个人能力,只是最基本的原理和使用方法,只能起到一个抛砖引玉的作用。Redux还有许多值得深究的地方,比如selector我只是一笔带过,但其实它也是有个发展历程的。今后如果还有机会,会在React状态管理这一块做更深入的探讨。

一些感想

刚刚开始学习React的时候,我从来没有考虑过如何去做数据管理,更没想过框架是如何产生的,只是机械的用别人封装好的方法和框架,只考虑如何实现功能。但是随着逐渐深入,我接触了公司几个不同的业务,我发现他们的数据管理方式各不相同,我开始去接触ReduxFlux这些状态管理的框架,在其中也确实得到了技术的锻炼和提升。

最后和大家分享一下男神尤雨溪的知乎回答。

“一直呆在舒适区往往就得不到提升,程序员能力的提升往往都发生在尝试解决一个从没解决过的问题之后进行反思的过程中。”

共勉。