React-Redux源码解析

237 阅读4分钟

React-Redux简介和运用

官方文档

image.png

容器组件/展示组件

  Redux的React绑定库是基于 容器组件和展示组件分类 的开发思想。React-Redux将所有组件分成两大类: UI组件(presentational component)容器组件(container component)

  • UI组件

    • 只负责UI的呈现,不带有任何业务逻辑
    • 没有状态
    • 所有数据都由参数(this.props)提供
    • 不使用任何 Redux的API
  • 容器组件

    • 负责管理数据和业务逻辑,不负责UI的呈现
    • 带有内部状态
    • 使用Redux 的API

    UI组件负责UI的呈现,容器组件负责管理数据和逻辑 ,两者的关联由 connect 进行连接。

import { createStore } from "redux";

// 创建一个reducer,定义state中的修改规则
const countReducer = function(count = 0, action) {
  switch(action.type) {
    case 'ADD':
      return count + 1;
    case 'MINUS':
      return count - 1;
    default:
      return count;
  }
}


// 创建一个store来存储state
const store = createStore(countReducer);

export default store;

Provider

  <Provider store> 使组件层级中的 connect() 方法都能够获得 Redux store。正常情况下,你的根组件应该嵌套在 <Provider> 中才能使用 connect() 方法。

  Provider实际上就是Context<Context.Provider value={store}>创建的组件。将Redux store传入到子组件中。

import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import ReactReduxPage from "./pages/ReactRedux/ReactReduxPage";

import store from './store';

ReactDOM.render(
  <Provider store={store}>
    <div className="center">
      {/* <SetStatePage /> */}
      {/* <ContextPage /> */}
      {/* <HookPage /> */}
      {/* <ReduxPage /> */}
      <ReactReduxPage />
    </div>
  </Provider>,
  document.getElementById("root")
);

connect()

connect([mapStateToProps], [mapDispatchToProps])(WrappedComponent)

  • [mapStateToProps(state, [ownProps]): stateProps] (Function): 如果定义该参数,组件将会监听 Redux store 的变化。任何时候,只要 Redux store 发生改变,mapStateToProps 函数就会被调用。该回调函数必须返回一个纯对象
  • [mapDispatchToProps(dispatch, [ownProps]): dispatchProps] (Object or Function): 如果传递的是一个对象,那么每个定义在该对象的函数都将被当作 Redux action creator,对象所定义的方法名将作为属性名;每个方法将返回一个新的函数,函数中dispatch方法会将 action creator 的返回值作为参数执行。这些属性会被合并到组件的 props
// UI组件
import { Component } from "react";
import { connect } from "react-redux";
import { mapStateToProps, mapDispatchToProps } from "./reactRedux";

class ReactReduxPage extends Component {
  render() {
    console.log(this.props);
    return (
      <div>
        ReactReduxPage - store - {this.props.count}
        <button onClick={this.props.addCount}>新增</button>
        <br />
        <button onClick={this.props.minusCount}>减少</button>
      </div>
    );
  }
}

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

// 使用connect连接生成容器组件
// reactRedux.js
export const mapStateToProps = (state) => {
  return {
    count: state,
  };
};

export const mapDispatchToProps = (dispatch) => {
  return {
    addCount: () => dispatch({ type: "ADD" }),
    minusCount: () => dispatch({ type: "MINUS" }),
  };
};

React-Redux源码实现

Provider

  Provider 实际上是在根组件外面包了一层,将 store 放在上下文 context 中。这样所有的子组件都可以从 context 中拿到 store

 // context.js
 import React from 'react';
 const Context = React.createContext();
 
 export default Context;
 
 // Provider.js
import Context from './context';
export default function Provider({ children, store }) {
    // Provider的原理实际是Context属性
    return <Context.Provider value={store}>{children}</Context.Provider>
}

connect

  • 第一版
import { useContext } from 'react';
import { bindActionCreators } from '../Redux';
import Context from './context';
/**
 * connect实际上是一个高阶组件,将store的数据挂载到wrappedComponent上
 * @param {Function} mapStateToProps
 * @param {Function | Object} mapDispatchToProps
 * @returns
 */
const connect =
  (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => (props) => {
    let stateProps = {},
      dispatchProps = {};
    const store = useContext(Context);
    const dispatch = store.dispatch;
    const subscribe = store.subscribe;

    if(typeof mapStateToProps !== 'function') {
      throw new Error('mapStateToProps必须是一个函数!');
    }

    if (typeof mapDispatchToProps !== 'object' && typeof mapDispatchToProps !== 'function') {
      throw new Error('mapDispatchToProps必须为Function或者Object');
    }

    stateProps = mapStateToProps(store.getState());

    // 函数中dispatch方法会将 action creator 的返回值作为参数执行,得到actionCreators
    if (typeof mapDispatchToProps === 'function') {
      dispatchProps = mapDispatchToProps(dispatch);
    } else if (typeof mapDispatchToProps === 'object') {
      dispatchProps = bindActionCreators(mapDispatchToProps, dispatch);
    }

    return <WrappedComponent {...props} {...stateProps} {...dispatchProps} />;
  };
export default connect;

image.png

  当前版本所存在的问题,点击按钮,store中的states树会变化,但是视图不会重新渲染。

  原因: store数据的变化无法触发视图更新,使用hooks自定义一个触发视图更新的forceUpdate。参考官方文档FAQ中的问题:有类似 forceUpdate 的东西吗?

// 优化后的connect.js
import { useContext, useEffect, useReducer } from "react";
import { bindActionCreators } from "../Redux";
import Context from "./context";
/**
 * connect实际上是一个高阶组件,将store的数据挂载到wrappedComponent上
 * @param {Function} mapStateToProps
 * @param {Function | Object} mapDispatchToProps
 * @returns 返回一个组件
 */
const connect =
  (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => (props) => {
    let stateProps = {},
      dispatchProps = {};
    const store = useContext(Context);
    const dispatch = store.dispatch;
    const subscribe = store.subscribe;

    if (typeof mapStateToProps !== "function") {
      throw new Error("mapStateToProps必须是一个函数!");
    }

    if (
      typeof mapDispatchToProps !== "object" &&
      typeof mapDispatchToProps !== "function"
    ) {
      throw new Error("mapDispatchToProps必须为Function或者Object");
    }

    stateProps = mapStateToProps(store.getState());

    // 函数中dispatch方法会将 action creator 的返回值作为参数执行,得到actionCreators
    if (typeof mapDispatchToProps === "function") {
      dispatchProps = mapDispatchToProps(dispatch);
    } else if (typeof mapDispatchToProps === "object") {
      dispatchProps = bindActionCreators(mapDispatchToProps, dispatch);
    }

    const [ignore, forceUpdate] = useReducer((x) => x + 1, 0);

    // 不使用useEffect: useEffect异步执行, 可能会导致丢失一些数据
    useLayoutEffect(() => {
      const unsubscribe = subscribe(() => {
        forceUpdate();
      });
      // 返回一个清除函数
      return () => {
        // 视图卸载之后将订阅删除
        if (unsubscribe) {
          unsubscribe();
        }
      };
    }, [subscribe]);

    return <WrappedComponent {...props} {...stateProps} {...dispatchProps} />;
  };
export default connect;

useStore

  useStore非常简单

// hooks.js
import Context from './Context';
function useStore() {
    return useContext(Context);
}

useDispatch

  useDispatch也比较简单,在useStore的基础上提取出dispatch返回即可

// hooks.js

export function useDispatch() {
  const { dispatch } = useStore();
  return dispatch;
}

useSelector

  • 初版
// hooks.js
export function useSelector(selector) {
  const store = useStore();
  const { getState } = store;
  const selectedState = selector(getState());

  return selectedState;
}

// function组件中的使用
import { useStore, useDispatch, useSelector } from "../../React-Redux";
export default function FunctionReactRedux() {
  const store = useStore();
  const dispatch = useDispatch();
  console.log('store', store);
  const handleAdd = () => {
    dispatch({ type: 'ADD' })
  }

  const count = useSelector(state => state.count);
  return <div>function - Children - {count} <button onClick={handleAdd}>增加</button></div>
}

image.png

  初版的selectorconnect初版存在同样的问题,无法触发视图的更新。使用同样的解决办法即可

  • 优化版本
export function useSelector(selector) {
  const store = useStore();
  const { getState, subscribe } = store;
  const selectedState = selector(getState());

  const [ignore, forceUpdate] = useReducer(x => x+1, 0);

  // 不使用useEffect: useEffect异步执行, 可能会导致丢失一些数据
    useLayoutEffect(() => {
    const unsubscribe = subscribe(() => {
      forceUpdate(); // 实际为dispatch  触发视图更新
    });
    return () => {
      // 视图卸载之后将订阅删除
      if (unsubscribe) {
        unsubscribe();
      }
    }
  }, [subscribe]);

  return selectedState;
}