实现简易版react-redux

246 阅读6分钟

时隔几个月,我又来更新react相关啦,上一次更新的是redux及其中间件,这一次我将会更新react-redux。

相信大家用过redux的都知道,在react中使用redux是非常繁琐的,我们在组件内部需要对其进行一个subscribe的订阅操作,所以对于开发人员来说这个很不友好,这个时候就出现了react-redux这么个连接库,用于帮助我们连接react和redux,简化我们的开发流程。

我们来写一个react-redux的简单使用demo,首先创建我们的store:

// src/store/index

import {createStore, combineReducers} from "redux";

// 定义修改规则
export const countReducer = (state = 0, {type, payload = 1}) => {
  switch (type) {
    case "ADD":
      return state + payload;
    case "MINUS":
      return state - payload;
    default:
      return state;
  }
};

// 创建一个数据仓库
const store = createStore(combineReducers({count: countReducer}));

export default store;

然后在根节点通过react-redux的Provider向下传递我们的store:

import {Provider} from "./myReactRedux";

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

当我们要在class组件里使用的时候则需要用connect去进行绑定我们组件:

import React, {Component} from "react";
import {bindActionCreators, connect} from "../myReactRedux";

// hoc是个函数,接收组件作为参数,返回新的组件
@connect(
  // mapStateToProps 把state map(映射) props上一份
  ({count}) => ({count}),

  // mapDispatchToProps object | function
  {
    add: () => ({type: "ADD"}),
    minus: () => ({type: "MINUS"}),
  }
  // (dispatch) => {
  //   let creators = {
  //     add: () => ({type: "ADD"}),
  //     minus: () => ({type: "MINUS"}),
  //   };

  //   creators = bindActionCreators(creators, dispatch);

  //   return {dispatch, ...creators};
  // }
)
class ReactReduxPage extends Component {
  render() {
    console.log("props", this.props); //sy-log
    const {count, dispatch, add, minus} = this.props;
    return (
      <div>
        <h3>ReactReduxPage</h3>
        <p>{count}</p>
        <button onClick={() => dispatch({type: "ADD", payload: 100})}>
          dispatch add
        </button>

        <button onClick={add}> add </button>
        <button onClick={minus}> minus </button>
      </div>
    );
  }
}
export default ReactReduxPage;

上面的代码中我们实现了两个type分别是"ADD"和"MINUS",如果我们只用redux的话是需要在组件对store进行订阅的,但是有了connect组件了之后则不需要订阅。

那么我们按照react-redux的用法来一一手写实现吧,首先是Provider,这里的Provider组件的用法是不是很熟悉呢,没错它和React.createContext非常的像,其实Provider组件内部也是使用的Context去进行实现的:

//* 创建一个Context对象
const Context = React.createContext();

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

上面的代码中我们先创建了一个Context对象,然后函数Provider返回了一个Context.provider组件,将我们的创建的store对象给传递下去,这就是redux.provider组件。

然后我们来实现我们使用的connect函数,这个是利用HOC(高阶组件)的思想,接收组件,并且返回一个组件,对这个组件进行装饰,其实这个也是一种很经典的设计模式,装饰器模式。同时也利用了函数的柯里化对参数进行拆分,我们的Connect接收两个参数,分别为mapStateToPropsmapDispatchToProps,然后再接收一个组件作为参数,而组件也需要接收向下传递下来的props,在上面的ReactReduxPage组件中,我们获取state和dispatch都是从this.props中获取,我们要获取所以依托于这个思路我们可以写出如下代码:

// ./myReactRedux

export const connect = (mapStateToProps, mapDispatchToProps) => (
  WrappedComponent
) => (props) => {

  // todo
  return <WrappedComponent {...props} {...stateProps} {...dispatchProps} />;
}

最后返回的组件有三个参数,首先是props这个是原本组件的props,然后stateProps和dispatchProps则是我们需要去进行处理的,而这两个都是在store里面,所以我们要获取到我们之前创建的Context传递下来的store,这里我们用hooks的useContext拿到store,我们可以观察到我们传进来的mapStateToProps是一个函数,函数返回的是我们要拿到的state的解构值,所以我们可以直接调用这个函数,而参数则是store.getState():

export const connect = (mapStateToProps, mapDispatchToProps) => (
  WrappedComponent
) => (props) => {

  // 子孙组件接收跨层级传递下来的store
  const store = useContext(Context);

  const stateProps = mapStateToProps(store.getState());

  // todo
  return <WrappedComponent {...props} {...stateProps} {...dispatchProps} />;
}

接下来就是获取dispatchProps,获取dispatch自然要从store.dispatch获取,这里传入的mapDispatchToProps有两种传入方式,分别为Object || Function,所以我们要对他们分别进行判断:

export const connect = (mapStateToProps, mapDispatchToProps) => (
  WrappedComponent
) => (props) => {

  // 子孙组件接收跨层级传递下来的store
  const store = useContext(Context);

  const stateProps = mapStateToProps(store.getState());

  let dispatchProps = {dispatch: store.dispatch};
  
  if (typeof mapDispatchToProps === "function") {
    dispatchProps = mapDispatchToProps(store.dispatch);
  } else if (typeof mapDispatchToProps === "object") {
    dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch);
  }
  // todo
  return <WrappedComponent {...props} {...stateProps} {...dispatchProps} />;
}

如果mapDispatchToProps传进来的是一个函数则将函数直接执行,参数为就是我们获取到的dispatchProps,如果传进来的是一个对象,那么我们就需要用bindActionCreators把它去包一层,那么这个bindActionCreators这个函数是干嘛的呢?他是把一个 value 为不同 action creator 的对象,转成拥有同名 key 的对象。同时使用 dispatch 对每个 action creator 进行包装,以便可以直接调用它们。是不是听起来有点抽象,没事我们用代码来实现:

function bindActionCreator(creator, dispatch) {
  return (...args) => dispatch(creator(...args));
}

export function bindActionCreators(creators, dispatch) {
  let obj = {};

  // todo 遍历creators
  for (let key in creators) {
    obj[key] = bindActionCreator(creators[key], dispatch);
  }

  return obj;
}

这里返回之后,我们在class组件里通过解构this.props拿到的add或者minus就是一个(...args) => dispatch(creator(...args))这样的函数,当我们去调用add的时候就直接调用了(...args) => dispatch(() => ({type: "ADD"})(...args))dispatch({type: 'ADD'})

书接上回言归正传,我们的connect函数还没有写完,我们还缺少了最重要的一个东西,那就是redux的订阅操作,没有订阅咋更新组件呢,这里我们用到了一个hooks的小技巧,即用useState或者useReducer模拟this.forceUpdate去强制更新整个组件,是组件渲染最新的store的值:

export const connect = (mapStateToProps, mapDispatchToProps) => (
  WrappedComponent
) => (props) => {
  // 子孙组件接收跨层级传递下来的store
  const store = useContext(Context);

  const stateProps = mapStateToProps(store.getState());

  let dispatchProps = {dispatch: store.dispatch};
  if (typeof mapDispatchToProps === "function") {
    dispatchProps = mapDispatchToProps(store.dispatch);
  } else if (typeof mapDispatchToProps === "object") {
    dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch);
  }
  // const [state, setState] = useState(0);
  const [, forceUpdate] = useReducer((x) => x + 1, 0);

  // useEffect__(订阅)DOM变更
  // useLayoutEffect-DOM变更
  useLayoutEffect(() => {
    store.subscribe(() => {
      forceUpdate();
    });
  }, [store]);

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

有的人就会问了,这里为啥是useEffect,因为要做到及时更新dom,用useEffect的话,在副作用强制更新dom之前数据又变化了呢,那岂不是漏了更新。

写完了如何在class组件里更新的,那么让我们来看看它在hooks里组件是怎么更新的吧。先写一个函数组件例子:

import {useCallback} from "react";
// import {useDispatch, useSelector} from "react-redux";
import {useDispatch, useSelector} from "../myReactRedux";

function ReactReduxHookPage(props) {
  const count = useSelector(({count}) => count);
  const dispatch = useDispatch();

  const add = useCallback(() => {
    dispatch({type: "ADD"});
  }, []);

  return (
    <div>
      <h3>ReactReduxHookPage</h3>
      <button onClick={add}>{count}</button>
    </div>
  );
}
export default ReactReduxHookPage;

可以看到,这里是用了useSelector来获取state和用了useDispatch来获取dispatch,那么我们就根据这个思路来实现一下这两个函数吧:

function useReduxContext() {
  const store = useContext(Context)
  return {store}
}

function useStore() {
  const { store } = useReduxContext()
  return store
}

function useDispatch() {
  const store = useStore()
  return store.dispatch
}

function useSelector(selctor) {
  const store = useStore()
  const selectedState = selctor(store.getState())
  
  const [, forceUpdate] = useReducer((x) => x + 1, 0);

  // useEffect__(订阅)DOM变更
  // useLayoutEffect-DOM变更
  useLayoutEffect(() => {
    store.subscribe(() => {
      forceUpdate();
    });
  }, [store]);
  
  return selectedState
}

好了,到这里我们的简版react-redux就实现啦,相信看完整篇文章的大伙都收获不小吧!