时隔几个月,我又来更新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接收两个参数,分别为mapStateToProps和mapDispatchToProps,然后再接收一个组件作为参数,而组件也需要接收向下传递下来的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就实现啦,相信看完整篇文章的大伙都收获不小吧!