Skip to content

可预测的状态管理库 - redux

Redux 是 JavaScript 状态容器,提供可预测化的状态管理。中文文档

什么是可预测的状态管理

数据在 什么时候, 因为什么, 发生了 什么改变, 都是 可以控制和最终的, 我们就称之为 可预测的状态管理

为什么使用 redux?

  1. React 是通过数据来驱动界面更新的, React 负责更新界面, 而我们只需要负责管理相应的数据(状态)
  2. 随着页面越来越复杂, 我们需要在不同的组件中共享一些状态, 而且状态还可能存在依赖关系
  3. 在程序相对复杂的时候, 想要很要很好的知道 什么时候改变 为什么改变 改变了什么状态 就需要用到 redux

redux 三大核心

  1. 通过 Store 来保存状态
  2. 所有状态不能直接修改, 只能通过 action 来修改
  3. 通过 reducer 将 store 和 action 串联起来

redux-flow

redux 基本使用

js
const redux = require("redux");

// 1.定义一个状态
const initState = {
  count: 0,
};

// 2.利用 store 来保存状态
const store = redux.createStore(reducer);

// 3.利用 action 修改状态
const incrmentAction = { type: "incrment", payload: { num: 1 } };
const decrmentAction = { type: "decrment", payload: { num: 1 } };

// 4.利用 reducer 串联 store 和 action
function reducer(state = initState, action) {
  const { type, payload } = action;
  const newState = {};
  newState.count = state.count;
  switch (type) {
    case "incrment": // +1
      newState.count += payload.num;
      break;
    case "decrment": // -1
      newState.count -= payload.num;
      break;
  }

  return newState;
}

// 5.监听 state 状态变化
store.subscribe(() => {
  // 获取 store 中的 state 状态
  console.info("state 发生变化了:", store.getState());
});

// 触发修改, 让 state 中的值发生变化
store.dispatch(incrmentAction);

react-redux 和 redux 的关系

  1. redux 是一个状态管理的库, 在任何 js 环境中都能使用
  2. react-redux 是一个 基于 redux 并且专为 react 设计的状态管理库

在 react 中使用 react-redux

文档资料

安装

sh
yarn add redux react-redux
# or
npm i redux react-redux

创建相关文件

react-store

代码实现

组件部分

  • src/index.js
jsx
import React from "react";
import { render } from "react-dom";
import { Provider } from "react-redux";
import store from "./store/index";
import App from "./App.jsx";

render(
  <Provider store={store}>
    <App />
  </Provider>
  document.getElementById("root")
);
  • src/app.jsx
jsx
import React from "react";
import { connect } from "react-redux";
import { createTodoAction } from "./store/action";
import Todos from "./Todos";

class App extends React.Component {
  constructor(props) {
    super(props);
    this.todoRef = React.createRef();
  }

  render() {
    return (
      <div>
        <Todos />
        <div>
          <input ref={this.todoRef} type="text" />
          <button onClick={() => this.addTodo()}>添加任务</button>
        </div>
      </div>
    );
  }

  // 添加TODO
  addTodo() {
    const content = this.todoRef.current.value.trim();
    if (!content) {
      alert("任务内容不能为空");
      return;
    }

    const todos = this.props.todos || [];
    const isExists = todos.find((item) => item.content === content);
    if (isExists) {
      alert("任务已经存在");
      return;
    }

    const lastId = todos[todos.length - 1].id || 0;
    this.props.createTodo(lastId + 1, content);
    this.todoRef.current.value = "";
  }
}

// 将 store 中保存的 todos 映射到 props 上
// 也就是说, 在组件中可以直接使用 this.props.todos 就能
// 直接访问到 store 中保存的 todos
const mapStateToProps = (state) => {
  return {
    todos: state.todos,
  };
};

// 因为是直接返回一个对象: 有的人会利用 js 箭头函数的特性,
// 省略 retuen 语句, 但只写 {} 就无法识别, 所以必须 => ({})
// 使用 connect 将这个组件和react-redux连接起来后, 会
// 将这个 mapDispatchToProps 中返回对象上的所有方法
// 映射到组件的 props 上, 也就是说可以使用 this.props.createTodo
// 来调用这个方法
const mapDispatchToProps = (dispatch) => ({
  createTodo: (id, content) => dispatch(createTodoAction({ id, content })),
});

export default connect(mapStateToProps, mapDispatchToProps)(App);
  • src/Todos.jsx
jsx
import React from "react";
import { connect } from "react-redux";
import { removeTodoAction } from "./store/action";

class Todos extends React.PureComponent {
  render() {
    const todos = this.props.todos || [];
    return (
      <ul>
        {
          /* 直接遍历 props 中的 todos */
          todos.map((item) => {
            return (
              <li key={item.id}>
                <span>{item.content}</span>
                <button onClick={() => this.props.removeTodoById(item.id)}>
                  删除
                </button>
              </li>
            );
          })
        }
      </ul>
    );
  }
}

const mapStateToProps = (state) => ({
  todos: state.todos,
});

const mapDispatchToProps = (dispatch) => ({
  removeTodoById: (id) => dispatch(removeTodoAction(id)),
});

export default connect(mapStateToProps, mapDispatchToProps)(Todos);

store 部分

  • src/store/index.js
js
import { createStore } from "redux";
import reducer from "./reducer.js";

const store = createStore(
  reducer,
  // 检查是否安装调试工具, 开启调试
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

export default store;
  • src/store/reducer.js
js
import * as types from "./types";

const initState = {
  todos: [
    /*{ id: 101, content: "今天学习react-redux" }*/
  ],
};

const reducer = (state = initState, action) => {
  const { type, payload } = action;
  const newState = JSON.parse(JSON.stringify(state));
  switch (type) {
    case types.CREATE_TODO: // 增加 TODO
      newState.todos.push(payload);
      break;

    case types.REMOVE_TODO: // 删除 TODO
      newState.todos = newState.todos.filter((item) => item.id !== payload.id);
      break;

    default:
      console.info("未知的 Action Type!", type);
  }

  return newState;
};

export default reducer;
  • src/store/types.js
js
// 增加TODO
export const CREATE_TODO = "create_todo";

// 删除TODO
export const REMOVE_TODO = "remove_todo";
  • src/store/action.js
js
import * as types from "./types";

// 创建 todo
export const createTodoAction = (payload) => ({
  type: types.CREATE_TODO,
  payload,
});

// 删除 todo
export const removeTodoAction = (id) => ({
  type: types.REMOVE_TODO,
  payload: { id },
});

react-redux 的实现原理

js
import ReactDOM from "react-dom";
import React from "react";

const StoreCtx = React.createContext({});

export const Provider = StoreCtx.Provider;
export const connect = (mapStateToProps, mapDispatchToProps) => (Wrapper) => {
  class AdvComponent extends React.PureComponent {
    constructor(props, ctx) {
      super(props, ctx);
      this.state = {
        storeState: { ...mapStateToProps(this.context.getState()) },
      };
    }

    componentDidMount() {
      this.context.subscribe(() => {
        this.setState({
          storeState: { ...mapStateToProps(this.context.getState()) },
        });
      });
    }

    componentWillUnmount() {
      this.context.unsubscribe();
    }

    render() {
      return (
        <Wrapper
          {...this.props}
          {...mapStateToProps(this.context.getState())}
          {...mapDispatchToProps(this.context.dispatch)}
        />
      );
    }
  }

  AdvComponent.contextType = StoreCtx;
  return AdvComponent;
};

使用与官方 react-redux 不同的是:

jsx
import React from "react";
import { render } from "react-dom";
import App from "./App.jsx";
import { Provider } from "./react-redux";
import store from "./store/index";

render(
  <React.StrictMode>
    {/* 注意: 此处用的是 value 而不是 store, 其他的和官方的 react-redux 使用没有什么不同 */}
    <Provider value={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById("root")
);

redux-thunk 中间件

什么是中间件, 为什么要使用中间件?

可以在 action 被派发到 reducer 之前做一些额外的操作, 这个操作的函数就叫中间件

使用中间件, 可以降低代码耦合性, 提高代码的可维护性

redux-middleware

redux-thunk github

默认情况下, dispatch 方法只能接收一个对象, 如果想让 dispatch 除了可以接收对象以外还可以接收方法, 那么我们可以使用 redux-thunk 中间件 redux-thunk 中间件的作用: 可以让 dispatch 接收一个函数, 可以让我们在通过 dispatch 派发任务的时候去执行这个传递的函数

1.安装

sh
yarn add redux-thunk
# or
npm  i redux-thunk

2.在 store 应用中间件 src/store/index.js

js
import { createStore, applyMiddleware, compose } from "redux";
import reducer from "./reducer.js";
import ReduxThunk from "redux-thunk";

// 检测是否有浏览器调试插件
const composeEnhancers =
  typeof window === "object" && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
    ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
    : compose;

// 应用中间件
const enhancer = composeEnhancers(applyMiddleware(ReduxThunk));

const store = createStore(reducer, enhancer);

export default store;
  1. 使用
  • src/App.jsx
jsx
import React from "react";
import { connect } from "react-redux";
import { getRecomPlayListAction } from "./store/action";

class App extends React.Component {
  render() {
    return (
      <div>
        <div>
          <button onClick={() => this.props.getRecomPlayList()}>
            获取网易云音乐推荐歌单
          </button>
        </div>

        {/* 获取到的数据: */}
        <div>
          <pre>{JSON.stringify(this.props.recomPlayList, "", 4)}</pre>
        </div>
      </div>
    );
  }
}

const mapStateToProps = (state) => ({
  recomPlayList: state.recomPlayList,
});

const mapDispatchToProps = (dispatch) => ({
  getRecomPlayList: () => dispatch(getRecomPlayListAction),
});

export default connect(mapStateToProps, mapDispatchToProps)(App);
  • src/store/action.js
js
import * as types from "./types";

/**
 * 获取推荐歌单 action, 如果已经获取了, 就不获取了
 * @param {Function} dispatch 派发 action
 * @param {Function} getState 用于获取 store 中的 state
 */
export const getRecomPlayListAction = (dispatch, getState) => {
  const state = getState();
  if (state.recomPlayList.length) {
    return;
  }

  const url = "http://musicapi.liaohui5.cn/personalized?limit=10";
  fetch(url)
    .then((res) => res.json())
    .then((data) => {
      const action = {
        type: types.GET_RECOM_PLAY_LIST,
        payload: [],
      };

      if (data.code !== 200) {
        console.log("响应数据有误", data);
      } else {
        action.payload = data.result;
      }

      dispatch(action);
    })
    .catch((error) => {
      console.info("请求出错了...", error);
    });
};
  • src/store/reducer.js
js
import * as types from "./types";

const initState = {
  recomPlayList: [], // 推荐歌单
};

const reducer = (state = initState, action) => {
  const newState = { ...state };
  const { type, payload } = action;

  switch (type) {
    case types.GET_RECOM_PLAY_LIST: // 获取推荐歌单
      newState.recomPlayList = payload;
      break;

    default:
      console.info("未知的 Action Type!", type);
  }

  return newState;
};

export default reducer;

redux-saga 中间件

官方文档github

  • redux-saga 和 redux-thunk 功能类似, 只是使用方法上有些不同

自定义 redux 中间件

js
/**
 * 中间件的本质就是一个函数, 如果在 store 中应用这个中间件,
 * 那么在所有的 action 派发后, 不会直接执行 reducer 而是会
 * 先依次执行完所有的中间件, 然后再执行 reducer
 *
 * @param {Object} store 包含两个方法, dispatch 和 getState
 * @param store.dispatch: 派发 action
 * @param store.getState: 获取当前 store 的所有状态
 */
let reduxLogger = (store) => {
  /**
   * @param {Function} next 如果本中间件执行完了, 需要调用这个方法去执行下一个中间件
   */
  return (next) => {
    /**
     * @param action 当前 action 信息
     */
    return (action) => {
      // 中间件的逻辑代码
    };
  };
};

// 这种写法是上面那种写法的简写形式, 上面那种写法只是为了写注释 ٩(๑>◡<๑)۶
reduxLogger = (store) => (next) => (action) => {
  console.log(`${action.type}执行前的 state:`, store.getState());
  next(action);
  console.log(`${action.type}执行后的 state:`, store.getState());
};

export default reduxLogger;
  • 在 store 中应用中间件
js
import { createStore, applyMiddleware } from "redux";
import reducer from "./reducer.js";
import ReduxThunk from "redux-thunk";
import ReduxLogger from "./reduxLogger";

const store = createStore(reducer, applyMiddleware(ReduxThunk, ReduxLogger));

export default store;

优化 reducer

  • 目录结构: 拆分 reducers 和 actions

combin-reducers

  • 合并 reducers
js
import { combineReducers } from "redux";
import homeReducer from "./home/reducer";
import aboutReducer from "./home/reducer";

// 将拆分开的 reducer 合并
const reducer = combineReducers({
  homeReducer,
  aboutReducer,
});

export default reducer;

redux 浏览器调试工具

redux-devtools-extension

Released under the MIT License.