带你学懂Redux-上篇

2,526 阅读8分钟

前言

本篇文章会结合React对Redux进行讲解介绍,所以你在阅读本文之前,最好能拥有一定的React知识储备,想要快速对React有良好的了解,可以参考React官方教程进行学习

如果你对文字阅读略显枯燥的话,可以切换到B站观看我费心录制的的视频进行学习——带你学懂Redux-上篇

本文所实现的源代码托管在 Github 上:mini-redux

认识Redux

Redux是什么

Redux 是 JavaScript 状态容器,它是一个纯JS写的公共库,提供可预测化的状态管理,你完全可以在React, Vue, Angular, jQuery中使用它。

我们先来看一下这张图,Redux的核心就跟这张图相关。希望阅读完本文后,每当你想起 Redux 时,脑海里就是下面这张图。

src=http___image.mamicode.com_info_201708_20180111002020575512.png&refer=http___image.mamicode.webp

为什么需要redux

  • JavaScript开发的应用程序, 已经变得非常复杂了:

    • JavaScript需要管理的状态越来越多, 越来越复杂了
    • 这些状态包括服务器返回的数据, 用户操作的数据等等, 也包括一些UI的状态
  • 管理不断变化的state是非常困难的:

    • 状态之间相互存在依赖, 一个状态的变化会引起另一个状态的变化, View页面也有可能会引起状态的变化
    • 当程序复杂时, state在什么时候, 因为什么原因发生了变化, 发生了怎样的变化, 会变得非常难以控制和追踪

为此,我们可能需要Redux来帮助我们管理应用状态。下面,我们一起来看看Redux究竟如何使用吧。

redux在React中的使用

  • store.js

我们需要创建一个store供App使用,且一个应用应该只有一个store,createStore后会提供三个函数,分别是getStatedispatchsubscribe

createStore:这个API接受reducer函数作为参数,返回一个store,主要功能都在这个store上。

import { createStore } from "redux";
import { countReducer } from "../reducers/countReducer";

const store = createStore(countReducer);

export default store;
  • countReducer.js

创建一个reducer,定义改变store state的规则,reducer是一个纯函数,接收一个旧state和action,根据定义的规则,返回一个新的state

 export function countReducer(state = { count: 0 }, action) {
  switch (action.type) {
    case "INCREASE":
      return { count: state.count + action.amount };
    case "DECREASE":
      return { count: state.count - action.amount };
    default:
      return state;
  }
}
  • App.js

创建一个React页面,在页面中引入刚刚我们刚刚创建的store,我们可以在初始化时通过store.getState拿到redux store中的state初始值,将其作为我们React组件的state初始值。

同时,页面需要在渲染前通过store.subscribe注册监听stote state值的变化,按钮点击后通过触发dispatch(action)后经由reducer处理后返回一个新的store state状态值后,刚刚注册监听的函数会执行拿到最新的store state。

import React from "react";
import store from "../store";

class App extends React.Component {
 constructor() {
   super();
   console.log(store.getState());
   this.state = {
     count: store.getState().count
   };
 }

 componentWillMount() {
   this.unsubscribe = store.subscribe(() => {
     this.setState({
       count: store.getState().count
     });
   });
 }

 componentWillUnmount() {
   this.unsubscribe();
 }

 render() {
   return (
     <div>
       <h3>ReduxDemo</h3>
       <p>{this.state.count}</p>
       <button onClick={() => store.dispatch({ type: "INCREASE", amount: 2 })}>
         +
       </button>
       <button onClick={() => store.dispatch({ type: "DECREASE", amount: 2 })}>
         -
       </button>
     </div>
   );
 }
}

export default App;

以上就是Redux在React中的最简应用了,你学废了吗

这个例子虽然很简短,但是已经包含了Redux的核心功能了(是真的啦),有没有发现我们只有一个createStore函数是来自于redux,那他究竟是个什么东东呢,在刚刚的例子中,他是怎么做到帮我们管理状态值的呢?所以接下来,我们一起来实现一个createStore吧。

我们再来回忆store上我们都用到了啥:

store.subscribe: 订阅state的变化,当state变化的时候执行回调,可以有多个subscribe,里面的回调会依次执行。

store.dispatch: 发出action的方法,每次dispatch(action)都会执行reducer生成新的state,然后执行subscribe注册的回调。

store.getState:一个简单的方法,返回当前的state

看到subscribe注册回调,dispatch触发回调,想到了什么,这不就是发布订阅模式吗?不太清楚的童鞋可以点击链接学习一下。 谈谈观察者模式和发布订阅模式

下面我们看看createStore内部是怎么实现的

实现createStore

  • createStore.js
export default function createStore(reducer) {
 let state;
 let listeners = [];

 function getState() {
   return state;
 }

 function dispatch(action) {
   state = reducer(state, action);
   listeners.forEach((listener) => listener());
 }

 function subscribe(listener) {
   listeners.push(listener);

   return () => {
     const index = listeners.indexOf(listener);
     listeners.splice(index, 1);
   };
 }

/** 为使得仓库有初始的状态值(reducer设定的初始值),需要自动dispatch(action)下 **/
 dispatch({ type: "@@redux/INIT" });

 return {
   getState,
   dispatch,
   subscribe,
 };
}

是不是并没有想象中那么复杂,不过如此嘛。

那么在实际工作中,我们的应用可能很复杂,需要管理的状态值很多,难道我们应用中所有页面用到的stote state需要update时,都通过dispatch(action)经由同一个reducer处理后返回一个新的state吗?其实这样当然没问题,但是随着我们的应用越来越复杂,所有的规则逻辑都写在一个reducer里,会特别的冗余,最终这个文件可能会有成千上万行,极其不便于维护以及团队协助。那么有什么办法呢?

当应用逻辑逐渐复杂的时候,我们就要考虑将巨大的 Reducer 函数拆分成一个个独立的单元。Redux 为我们提供了 combineReducers API,用来组合多个小的reducer,可以让我们为不同的模块写自己的reducer,最终将他们组合起来,下面我们先来看一下redux中的combineReducers怎么使用。

combineReducers使用

  • 添加numberReducer.js
export function numberReducer(state = 0, action) {
 switch (action.type) {
   case "ADD":
     return state + 1;
   case "DEDUCT":
     return state - 1;
   default:
     return state;
 }
}

  • 修改App.js
import React from "react";
import store from "../store";

class App extends React.Component {
 constructor() {
   super();
   console.log(store.getState());
   this.state = {
     count: store.getState().countState.count,
     number: store.getState().numberState,
   };
 }

 componentWillMount() {
   this.unsubscribe = store.subscribe(() => {
     // console.log(store.getState());
     this.setState({
       count: store.getState().countState.count,
       number: store.getState().numberState,
     });
   });
 }

 componentWillUnmount() {
   this.unsubscribe();
 }

 render() {
   return (
     <div>
       <h3>ReduxDemo</h3>
       <p>{this.state.count}</p>
       <p>{this.state.number}</p>
       <button onClick={() => store.dispatch({ type: "INCREASE", amount: 2 })}>
         +
       </button>
       <button onClick={() => store.dispatch({ type: "DECREASE", amount: 2 })}>
         -
       </button>
       <button onClick={() => store.dispatch({ type: "ADD" })}>+</button>
     </div>
   );
 }
}

export default App;
  • 修改store.js
import { createStore, combineReducers } from "redux";
import { countReducer } from "../reducers/countReducer";
import { numberReducer } from "../reducers/numberReducer";

// 创建store
const store = createStore(
 combineReducers({
   countState: countReducer,
   numberState: numberReducer,
 })
);

export default store;

通过以上的例子,combineReducers 主要有两个作用:

  1. 组合所有 reducer 的 state
  • countReducer 的 state 为:
state = {count: 0};
  • numberReducer 的 state 为:
state = 0
  • 那么通过 combineReducers 组合这两个 reducerstate 得到的最终结果为:
state = {
    countState: {count: 0}
    numberState: 0
};

这个通过 combineReducers 组合后的最终 state 就是存储在 Store 里面的对象状态树。

  1. 分发 dispatch 的 Action。

通过 combineReducers 组合countReducer和numberReducer之后,从 React 组件中 dispatch(action)会遍历检查countReducer和numberReducer,判断是否存在响应对应 action.typecase 语句,如果存在,所有的这些 case 语句都会响应。

实现combineReducers

  • combineReducers.js
const combineReducers = (reducers) => {
 return (state = {}, action) => {
   const newState = {};
   for (let key in reducers) {
     newState[key] = reducers[key](state[key], action);
   }
   return newState;
 };
};
export default combineReducers;

react-redux

react-redux是什么

React-ReduxRedux的官方React绑定库。它能够使你的React组件从Redux store中读取数据,并且向store分发actions以更新数据

为什么需要react-redux

React组件使用redux需要引入store,并且要手动调用store.subscript()监听store state变化,使用起来比较麻烦。为了让React跟Redux结合的更优雅,react-redux简化了这些步骤,使数据流管理使用起来更加方便。

react-redux是运用Provider将组件和store对接,使在Provider里的所有组件都能共享store里的数据

在容器组件中通过react-redux核心API connect来连接UI组件和reduxconnect是一个高阶函数,第一个参数接收的是两个回调函数,回调函数1:将接收一个state,然后返回一个对象对象中包含了UI组件想要的状态。回调函数2:接收一个dispatch,返回一个对象

react-redux使用

  • index.js
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { Provider } from "react-redux";
import store from "./store";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);
  • App.js
import { Component } from "react";
import { connect } from "react-redux";

const App = (props) => {
  const { count, dispatch, increaseHandler } = props;
  return (
    <div>
      <h3>ReactReduxDemo</h3>
      <button onClick={increaseHandler}>increaseHandler:{count}</button>
      <button
        onClick={() => {
          dispatch({ type: "DECREASE", amount: 1 });
        }}
      >
        dispatch: {count}
      </button>
    </div>
  );
};

//mapStateToProps mapDispatchToProps
export default connect(
  ({ countState }) => {
    return {
      count: countState.count,
    };
  },
  (dispatch) => {
    return {
      dispatch,
      increaseHandler: () => dispatch({ type: "INCREASE", amount: 2 }),
      decreaseHandler: () => dispatch({ type: "DECREASE", amount: 2 }),
    };
  }
)(ReactReduxDemo);

实现react-redux

  • react-redux/index.js
import React, { useContext, useEffect, useLayoutEffect, useState } from "react";

const Context = React.createContext();

export function Provider({ store, children }) {
  return <Context.Provider value={store}>{children}</Context.Provider>;
}

export const connect =
  (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => (props) => {
    const store = useContext(Context);
    const { getState, dispatch, subscribe } = store;

    const [state, setState] = useState(mapStateToProps(getState()));

    let dispatchProps = { dispatch };
    dispatchProps = mapDispatchToProps(dispatch);

    useLayoutEffect(() => {
      const unsubscribe = subscribe(() => {
        const newState = mapStateToProps(getState());
        setState(newState);
      });
      return () => {
        unsubscribe();
      };
    }, []);

    return (
      <WrappedComponent
        {...props}
        dispatch={dispatch}
        {...state}
        {...dispatchProps}
      />
    );
  };

export function useSelector(selector) {
  const store = useContext(Context);
  const [state, setState] = useState(selector(store.getState()));
  const { subscribe } = store;
  useEffect(() => {
    const unsubscribe = subscribe(() => {
      setState(selector(store.getState()));
    });
    return () => {
      unsubscribe();
    };
  }, []);

  return state;
}

export function useDispatch() {
  const store = useContext(Context);
  const { dispatch } = store;
  return dispatch;
}

React函数组件使用react-redux提供的Hooks

  • ReactReduxHookDemo.js
import { useCallback } from "react";
import { useSelector, useDispatch } from "react-redux";

const ReactReduxHookDemo = (props) => {
  const dispatch = useDispatch();
  const count = useSelector(({ countState }) => {
    return countState.count;
  });

  const increaseHandler = useCallback(() => {
    dispatch({ type: "INCREASE", amount: 2 });
  }, []);

  const decreaseHandler = useCallback(() => {
    dispatch({ type: "DECREASE", amount: 2 });
  }, []);

  return (
    <div>
      <h3>ReactReduxHookDemo</h3>
      <button onClick={increaseHandler}>点击+2 ---- {count}</button>
      <button onClick={decreaseHandler}>点击-2 ---- {count}</button>
    </div>
  );
};

export default ReactReduxHookDemo;

最后

目前我们触发dispatch(action)中action只能为一个形似{type: 'xxx', amount: ''}的对象,因为目前触发dispatch(action)都是直接经由reducer处理返回新的store state,实际开发场景中,我们可能点击一个按钮后需要通过发起一个Ajax的异步请求后,获取到结果后再更新store state,当然,我们可以在页面请求后再dispatch(action),那能否有更优雅的实现方式呢,答案是肯定的,这就引出一个新概念——redux中间件

所谓中间件,我们可以理解为拦截器,用于对某些过程进行拦截和处理,且中间件之间能够串联使用。在redux中,我们中间件拦截的是dispatch提交到reducer这个过程,从而增强dispatch的功能。下篇文章我们来分析实现稍困难一些的redux中间件,敬请期待......