手写React-Redux 以及 hooks

578 阅读4分钟

在上一篇文章中我们手写了一个极简的redux和中间件,并且在react项目中演示实际使用效果。其实redux是一个js库,为了在react上方便使用,Redux 的作者封装了一个 React 专用的库 React-Redux

今天我们仿照React-Redux做一个简版的实现。

React-Redux

使用React-Redux

首先我们创建一个react项目,然后加入Redux和React-Redux

create-react-app lreact-redux

yarn add redux react-redux

创建一个常规的redux页面,

src
 | store
    | index.js
 | pages
    | reduxPage.jsx

创建一个store,store/index.js:

import { combineReducers, createStore } from "redux";

function countReducer(state = 0, action) {
   switch (action.type) {
       case 'ADD':
           return state + action.payload;
       case 'MINUS':
           return state - action.payload;
       default:
           return state;
   }
}

const store = createStore(combineReducers({ count: countReducer }));

export default store;

交互页面,pages/reduxPage.jsx:

import React from "react";
import { connect } from "react-redux";

function ReduxPage(props) {
 const { count, add } = props;
 return (
   <div>
     <div>{count}</div>
     <button onClick={() => add(2)}>add</button>
   </div>
 );
}

export default connect(({ count }) => ({ count }), {
 add: (payload) => ({ type: "ADD", payload }),
})(ReduxPage);

修改src/App.js,提供Provider

import { Provider } from 'react-redux';
import './App.css';
import ReduxPage from './pages/reduxPage';
import store from "./store";

function App() {
  return (
    <div className="App">
      <Provider store={store}>
        <ReduxPage />
      </Provider>
    </div>
  );
}

export default App;

运行项目,可以看到如下画面:

image.png

每点击一次ADD可以看到数字增加2。这是基本的React-Redux用法。

可以看到,React-Redux首先是通过Provider将store传入子组件,而组件中再通过connect这个高阶组件获取state和dispatch,并且在store变更的时候自动重新渲染视图。这便是我们的实现思路。

实现React-Redux

在src下创建lReactRedux.js,在这里实现我们的redux:

export const Provider = (params) => {
    
}

export const connect = (params) => {
    
}

Provider的原理是Context,Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

import React from 'react';

const Context = React.createContext();

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

再来看这个connect,这是个柯里化函数,先后接受了3次参数,第1次是mapStateToProps, mapDispatchToProps,第二次是需要被包含的组件WrappedComponent,再之后是这个WrappedComponent组件本身需要的参数。

我们先实现mapStateToProps处理state:

export const connect = (mapStateToProps, mapDispatchToProps) => WrappedComponent => props => {
    // 首先获取store 和 state
    const store = useContext(Context);
    const state = store.getState();
    
    // 执行mapStateToProps
    let stateProps = mapStateToProps(state);

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

更改项目中Provider和connect的引用,再次运行项目,可以看到state正常显示,说明我们的实现起作用了,但是此时add还没有效果,还会报错,是因为我们还没有实现mapDispatchToProps的处理。

mapDispatchToProps如果没有传递的话,会默认给一个dispatch方法;如果传递的是一个函数,则会传入dispatch执行;如果是一个对象,那么会遍历对象,每个value都返回一个action,我们需要给他当作dispatch的参数执行,并返回一个新的函数:

export const connect = (mapStateToProps, mapDispatchToProps) => WrappedComponent => props => {
    // 首先获取store 和 state
    const store = useContext(Context);
    const state = store.getState();

    // 执行mapStateToProps
    let stateProps = mapStateToProps(state);

    // 默认传递dispatch
    let dispatchProps = store.dispatch;

    // 如果是函数,则会传入dispatch执行
    // 如果是对象,则遍历,给action包一层dispatch
    if (typeof mapDispatchToProps === 'function') {
        dispatchProps = mapDispatchToProps(store.dispatch);
    } else if (typeof mapDispatchToProps === 'object' && mapDispatchToProps !== null) {
        dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch);
    }

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

function bindActionCreators(creators, dispatch) {
    let obj = {};
    for (const key in creators) {
        obj[key] = bindActionCreator(creators[key], dispatch);
    }
    return obj;
}

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

这个时候再运行项目,点击add按钮发现虽然不再报错,但是毫无反应,这是因为我们没有监听state的改变去同步视图重新渲染。只需在connect里将强制更新视图的函数加入redux的subscribe列表中即可。

强制更新视图在类组件我们可以使用forceUpdate,但是在函数组件我们只能用useState或者useReducer做一个计数器来更新视图,因为如果前后两次的值相同,useState和useReducer Hook 都会放弃更新。

export const connect = (mapStateToProps, mapDispatchToProps) => WrappedComponent => props => {
    // 首先获取store 和 state
    const store = useContext(Context);
    const state = store.getState();

    // 执行mapStateToProps
    let stateProps = mapStateToProps(state);

    // 默认传递dispatch
    let dispatchProps = store.dispatch;

    // 如果是函数,则会传入dispatch执行
    // 如果是对象,则遍历,给action包一层dispatch
    if (typeof mapDispatchToProps === 'function') {
        dispatchProps = mapDispatchToProps(store.dispatch);
    } else if (typeof mapDispatchToProps === 'object' && mapDispatchToProps !== null) {
        dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch);
    }

    const forceUpdate = useForceUpdate();

    useLayoutEffect(() => {
        const unSubscribe = store.subscribe(forceUpdate);
        return () => {
            unSubscribe();
        };
    }, [store, forceUpdate]);

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

// 用useState或者useReducer做一个计数器来更新视图
function useForceUpdate() {
    const [, update] = useReducer((x) => x + 1, 0);
    const forceUpdate = useCallback(
        () => {
            update();
        },
        [],
    );
    return forceUpdate;
}

再次运行项目,功能一切正常。

到这里我们就实现了一个建议版本的React-Redux了,接下来我们可以再实现React-Redux的一些Hooks。

实现React-Redux的Hooks

useDispatch

用于获取dispatch的hook。

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

useSelector

useSelector和connect中的mapStateToProps的概念差不多,它能让我们获取store中的state,并自动订阅state的更新。

export function useSelector(Selector) {
    const store = useContext(Context);
    const selectedState = Selector(store.state);

    const forceUpdate = useForceUpdate();

    useLayoutEffect(() => {
        const unSubscribe = store.subscribe(forceUpdate);
        return () => {
            unSubscribe();
        };
    }, [store, forceUpdate]);

    return selectedState;
}

最后

以上就是全部内容了,手写了一个简易的React-Redux 以及 2个Hooks。

有任何问题都欢迎交流~

参考: