react-redux的学习

372 阅读5分钟

redux的学习

react-redux

通过上面的学习,我们可以将状态都交给redux进行管理。而且也发现了,redux并不是一个依赖react的库。

但是我们在react中进行状态的变更,还需要手动订阅,手动派发action,还是有一点不那么方便的。

我们希望可以自动派发和订阅状态的变化等,就需要用到一个库:react-redux

对于我们使用者来说是很容易的:

类组件中我们可以这样使用:

import { Provider, connect } from "react-redux";
import actionCreators2 from "./store/actionCreator/counter2";
class Counter2 extends Component {
  render() {
    console.log(this.props);
    return (
      <>
        <h2>counter: {this.props.counter}</h2>
        <button onClick={this.props.add2}>+1</button>
        <button onClick={() => this.props.addNum2(5)}>+5</button>
        <button onClick={this.props.minus2}>-1</button>
      </>
    );
  }
}
// 将state映射为组件的props -> {counter:0, add2, addNum2, ...} 返回的值会被展开一层
const mapStateToProps = (state) => state.counter2;
// 连接仓库和组件
Counter2 = connect(mapStateToProps, actionCreators2)(Counter2);

const App = () => {
  return (
    // 提供仓库
    <Provider store={store}>
      <Counter2 />
    </Provider>
  );
};

这样:我们只需要在类组件中,直接使用派发方法即可,而不需要自己手动的派发。也不需要订阅状态等。

Provider

Provider其实就是一个context,用来提供store。

ReactReduxContext:

import { createContext } from "react";

const ReactReduxContext = createContext();
export default ReactReduxContext;

provider:

import ReactReduxContext from "./reactReduxContext";
/**
 * 提供store
 * @param {*} param0
 * @returns
 */
export default function Provider({ store, children }) {
  return (
    <ReactReduxContext.Provider value={{store}}>
      { children }
    </ReactReduxContext.Provider>
  );
}

connect:

connect其实就是一个高阶组件,也是一个高阶函数。函数本身接收两个参数,返回值仍然是一个高阶函数,参数是组件,

先来看类组件的实现:

本质上就是对原有组件的扩展,我们把订阅状态,以及派发事件都自己封装在内部,外界组件直接通过props获取状态和调用方法派发即可,逻辑的处理封装在内部。

import { Component } from "react";
import ReactReduxContext from "./reactReduxContext";
import { bindActionCreators } from "../redux";
/**
 * 连接组件和仓库store
 * @param {*} mapStateToProps  state映射为props的函数
 * @param {*} mapDispatchToProps 派发的函数 映射为组件的props
 * @returns
 */
export default function connect(mapStateToProps, mapDispatchToProps) {
  return function (OldComponent) {
    return class extends Component {
      static contextType = ReactReduxContext;
      /**
       *
       * @param {*} props
       * @param {*} context 就是我们上下文Context
       */
      constructor(props, context) {
        super(props);
        // 拿到仓库
        const { store } = context;
        const { getState, subscribe, dispatch } = store;
        // 状态
        this.state = mapStateToProps(getState());
        this.unsubscribe = subscribe(() => {
          this.setState(mapStateToProps(getState()));
        });
        // 派发的函数
        this.dispatchProps = bindActionCreators(mapDispatchToProps, dispatch);
      }
      render() {
        return (
          <OldComponent
            {...this.props}
            {...this.state}
            {...this.dispatchProps}
          />
        );
      }
      componentWillUnmount() {
        // 取消订阅
        this.unsubscribe();
      }
    };
  };
}

useSelect useDispatch

上面是类组件的connect的使用方式。

现在前端越来越淡化类组件,大力推崇函数式组件。但是函数式组件的使用方法肯定是和类组件不相同的。我们肯定是需要借助hooks来实现我们想要的功能。

在函数式组件中需要使用两个hook,useSelect 和 useDispatch.

import { useSelector, useDispatch } from "react-redux";
/**
 * 函数式组件使用react-redux
 */
const Counter = () => {
  // 总状态中取出我们要使用的分状态
  const state = useSelector((state) => state.counter1);
  const dispatch = useDispatch();
  return (
    <div>
      <h2>counter: {state.counter}</h2>
      <button onClick={() => dispatch(actionCreators.add())}>+1</button>
      <button onClick={() => dispatch(actionCreators.addNum(5))}>+5</button>
      <button onClick={() => dispatch(actionCreators.minus())}>-1</button>
    </div>
  );
};

实现原理

useDispatch是很容易的,无非就是拿到dispatch方法:

import { useContext } from "react";
import ReactReduxContext from "../reactReduxContext";

export const useDispatch = () => {
  const { store } = useContext(ReactReduxContext);
  return store.dispatch;
};

useSelector方法会麻烦一些:

我们根据传入的函数,然后将拿到的状态传递给参数selector,这个函数的返回值就是分状态:

import { useContext } from "react";
import ReactReduxContext from "../reactReduxContext";

export const useSelector = (selector) => {
  const { store } = useContext(ReactReduxContext);
  const state = store.getState();
  const selectedState = selector(state);
  return selectedState;
};

如果此时我们在每次都打印store里的状态,发现此时每次在点击的时候,状态都会发生更改,但是组件并没有发生重新渲染,因为我们还没有监听状态的更新。

import { useRef, useReducer, useLayoutEffect, useContext } from "react";
import ReactReduxContext from "../reactReduxContext";
/**
 * 获取状态并订阅状态的变更
 * @param {*} selector
 * @returns
 */
export const useSelector = (selector, equalityFn = shallowEqual) => {
  const prevSelectedState = useRef(null);
  const { store } = useContext(ReactReduxContext);
  const state = store.getState();
  const selectedState = selector(state);
  // 为了状态更新 可以重新渲染组件
  const [, forceUpdate] = useReducer((x) => x + 1, 0); // 只是为了更新组件 没特别含义
  useLayoutEffect(() => {
    // 监听状态的变化 执行回调
    return store.subscribe(() => {
      // 获取最新的分状态
      const selectedState = selector(store.getState());
      // 对比上次和最新的状态 状态不一致才会更新组件
      if (!equalityFn(prevSelectedState.current, selectedState)) {
        console.log(1);
        forceUpdate();
        prevSelectedState.current = selectedState;
      }
    });
  }, [store]); // 一个应用只有一个store 一般不会出现store的更改
  return selectedState;
};

/**
 * 两个对象 浅层比较
 * @param {*} obj1
 * @param {*} obj2
 * @returns
 */
export function shallowEqual(obj1, obj2) {
  if (obj1 === obj2) return true;
  // 基本类型
  if (
    typeof obj1 !== "object" ||
    obj1 === null ||
    typeof obj2 !== "object" ||
    obj2 === null
  )
    return false;
  // 都是对象 且属性都存在
  const k1 = Object.keys(obj1);
  const k2 = Object.keys(obj2);
  if (k1.length !== k2.length) return false;
  for (const k of k1) {
    // 没有该属性在对象自身上 或者属性值不同
    if (!obj2.hasOwnProperty(k) || obj1[k] !== obj2[k]) {
      return false;
    }
  }
  return true;
}

此时就可以实现状态的改变使组件更新

但是在函数式组件内,派发action还是觉得有点麻烦,我们想像类组件一样更加方便一些,那么我们可以再封装一个hook,帮我们绑定actionCreatordispatch,然后在函数式组件中就可以直接调用方法了。

useBoundDispatch

这个方法在react-redux中并没有提供该hook

import { useContext } from "react";
import ReactReduxContext from "../reactReduxContext";
import { bindActionCreators } from "../../redux";
export const useBoundDispatch = (actionCreators) => {
  const { store } = useContext(ReactReduxContext);
  const boundActionCreators = bindActionCreators(
    actionCreators,
    store.dispatch
  );
  return boundActionCreators;
};

这样:在函数式组件中我们就可以这样使用:

const Counter = () => {
  // 总状态中取出我们要使用的分状态
  const state = useSelector((state) => state.counter1);
  // const dispatch = useDispatch();
  const boundActionCreator = useBoundDispatch(actionCreators);
  return (
    <div>
      <h2>counter: {state.counter}</h2>
      <button onClick={boundActionCreator.add}>+1</button>
      <button onClick={() => boundActionCreator.addNum(5)}>+5</button>
      <button onClick={boundActionCreator.minus}>-1</button>
    </div>
  );
};

connect的函数式实现

前面我们实现了connect方法,但是使用的是类组件的形式。其实我们还可以使用函数式组件实现同样的功能

export default function connect(mapStateToProps, mapDispatchToProps) {
  return function (OldComponent) {
    return (props) => {
      const { store } = useContext(ReactReduxContext);
      const { getState, subscribe, dispatch } = store;
      const prevState = getState();
      const stateToProps = useMemo(
        () => mapStateToProps(prevState),
        [prevState]
      );
      const dispatchProps = useMemo(() => {
        // 其实 mapDispatchToProps 是有多种写法的 7种 常见的有三种
        // 1. 对象 2. 函数 3. 什么都不传
        if (typeof mapDispatchToProps === "function") {
          return mapDispatchToProps(store.dispatch);
        } else if (
          mapDispatchToProps !== null &&
          typeof mapDispatchToProps === "object"
        ) {
          return bindActionCreators(mapDispatchToProps, dispatch);
        } else {
          return { dispatch }; // undefined
        }
      }, [store.dispatch]);
      // 订阅
      const [, forceUpdate] = useReducer((x) => x + 1, 0);
      useLayoutEffect(() => {
        return subscribe(forceUpdate);
      }, [subscribe]);
      return <OldComponent {...props} {...stateToProps} {...dispatchProps} />;
    };
  };
}