手写傻瓜式 React 全家桶之 Reduxjavascript
手写傻瓜式 React 全家桶之 React-Reduxhtml
本文代码前端
上一篇手写了 Redux 源码,同时也说明了 Redux 里头是没有 React 相关的 API,这篇我们来写下 React-Redux,那么 React,Redux 以及 React-Redux 关系是:java
上一篇使用 Redux 开发了个加减器的功能,可是暴露了几个问题:react
import store from "../store";
componentDidMount() {
this.unsubscribe = store.subscribe(() => {
this.forceUpdate();
});
}
componentWillUnmount() {
if (this.unsubscribe) {
this.unsubscribe();
}
}
复制代码
为了解决这些问题,react-redux 就应运而生了git
React-Redux 是链接 React 应用和 Redux 状态管理的桥梁。其中既有 React 的 API,也会依赖 Redux 的相关 API。其实 React-Redux 主要提供了两个 api:github
将根组件嵌套在 <Provider>
中,这样子孙组件就能经过 connect
获取到 stateweb
例子:redux
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { Provider } from "react-redux";
import store from "./store";
ReactDOM.render(
<React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode>,
document.getElementById("root")
);
复制代码
其中的 store
参数就是指 Redux 的 createStore
生成的 storeapi
connect
是个高阶组件,通过它包装后的组件将获取以下功能:
props
里会带有 dispatch
函数connect
传递了第一个参数,那么会将 store
里的 state
数据,映射到当前组件的 props
里connect
传递了第二个参数,那么会将相关方法,映射到当前组件的 props
里state
更改时,会通知当前组件更新,从新渲染视图语法:
function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?) 复制代码
默认 create-react-app 脚手架是不支持 @装饰器的,能够经过 react-app-rewired 优雅配制
接下来分别讲解下这四个参数
mapStateToProps:
const mapStateToProps = state => ({ count: state.count })
复制代码
该函数必须返回一个纯对象,这个对象会与组件的 props 合并。若是定义该参数,组件将会监听 Redux store 的变化,不然不监听。
mapDispatchToProps:
若是省略这个 mapDispatchToProps
参数,默认状况下,dispatch
会注⼊到你的组件 props
中。 该参数存在两种格式:
const mapDispatchToProps = {
add: () => ({ type: "ADD" }),
minus: () => ({ type: "MINUS" }),
};
复制代码
对象里的方法名会被合并到组件的 props
里,经过该方法名就能够触发相应的 action
对象的形式,没办法往 props
里注入 dispatch
,只能是具体的 action
操做
该函数将接收 dispatch
参数,而后返回任何要注入到 props 里的对象
const mapDispatchToProps = (dispatch) => ({
add: () => dispatch({ type: "ADD" }),
minus: () => dispatch({ type: "MINUS" }),
});
复制代码
上面这种写法有些复杂,能够采用 redux 提供的 bindActionCreators
简化下
const mapDispatchToProps = (dispatch) => {
let creators = {
add: () => ({ type: "ADD" }),
minus: () => ({ type: "MINUS" }),
};
creators = bindActionCreators(creators, dispatch);
return {
...creators,
dispatch,
};
};
复制代码
mergeProps:
mergeProps(stateProps, dispatchProps, ownProps)
复制代码
若是省略这个 mergeProps 参数,默认状况下,会返回 Object.assign({}, ownProps, stateProps, dispatchProps)
。
若是定义了这个参数,mapStateToProps()
与 mapDispatchToProps()
的执⾏结果和组件⾃身的 props
将传⼊到这个回调函数中。
该回调函数返回的对象将做为 props
传递到被包裹的组件中。
options:
{
context?: Object, // 自定义上下文
pure?: boolean, // 默认为 true , 当为 true 的时候 ,除了 mapStateToProps 和 props ,其余输入或者state 改变,均不会更新组件。
areStatesEqual?: Function, // 当pure true , 比较引进store 中state值 是否和以前相等。 (next: Object, prev: Object) => boolean
areOwnPropsEqual?: Function, // 当pure true , 比较 props 值, 是否和以前相等。 (next: Object, prev: Object) => boolean
areStatePropsEqual?: Function, // 当pure true , 比较 mapStateToProps 后的值 是否和以前相等。 (next: Object, prev: Object) => boolean
areMergedPropsEqual?: Function, // 当 pure 为 true 时, 比较 通过 mergeProps 合并后的值 , 是否与以前等 (next: Object, prev: Object) => boolean
forwardRef?: boolean, //当为true 时候,能够经过ref 获取被connect包裹的组件实例。
}
复制代码
mergeProps
与 options
比较少用到,重点关注前两个参数
示例代码:
import React, { Component } from "react";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
const mapStateToProps = (state) => ({ count: state.count });
// const mapDispatchToProps = {
// add: () => ({ type: "ADD" }),
// minus: () => ({ type: "MINUS" }),
// };
// const mapDispatchToProps = (dispatch) => ({
// add: () => dispatch({ type: "ADD" }),
// minus: () => dispatch({ type: "MINUS" }),
// });
const mapDispatchToProps = (dispatch) => {
let creators = {
add: () => ({ type: "ADD" }),
minus: () => ({ type: "MINUS" }),
};
creators = bindActionCreators(creators, dispatch);
return {
...creators,
dispatch,
};
};
@connect(mapStateToProps, mapDispatchToProps)
class Counter extends Component {
render() {
const { count, add, minus } = this.props;
return (
<div className="border"> <h3>加减器</h3> <button onClick={add}>add</button> <span style={{ marginLeft: "10px", marginRight: "10px" }}>{count}</span> <button onClick={minus}>minus</button> </div>
);
}
}
export default Counter;
复制代码
在函数组件里,除了使用 connect
方式接收传递的 state 与 dispatch 信息以外,React-Redux 还提供了两个 hook: useSelector
与 useDispatch
useSelector:
const result: any = useSelector(selector: Function, equalityFn?: Function)
复制代码
平时用的更多的是第一个参数,是个函数,参数为 store
的 state
const state = useSelector(({ count }) => ({ count }));
复制代码
返回个对象,key
为 count
,内容就是 store state
里的 count
。 这样经过 state.count
就能够获取到
useDispatch:
const dispatch = useDispatch()
复制代码
执行下 useDispatch
就获取到了 dispatch
,经过 dispatch
就能够更改状态
useStore:
const store = useStore()
复制代码
返回 store
对象的引用。尽可能不要使用该 hook
,useSelector
才是首选
示例代码:
import { useCallback } from "react";
import { useSelector, useDispatch } from "react-redux";
export default function ReactReduxHookPage() {
const state = useSelector(({ count }) => count);
const dispatch = useDispatch();
const add = useCallback(() => {
dispatch({ type: "ADD" });
}, []);
return (
<div> <h3>ReactReduxHookPage</h3> <p>{state}</p> <button onClick={add}>add</button> </div>
);
}
复制代码
provide
作的事情就是为后代组件提供 store ,这不正是 React context api
干的事 首先建一个 context 文件,导出须要用的 context :
import React from "react";
const ReactReduxContext = React.createContext();
export default ReactReduxContext;
复制代码
将 context 应用到 Provider
组件里
import ReactReduxContext from "./context";
export function Provider({ children, store }) {
return (
<ReactReduxContext.Provider value={store}> {children} </ReactReduxContext.Provider>
);
}
复制代码
能够看出 Provider 组件代码不难,无非就是将传进来的 store
做为 context
的 value
值,而后直接渲染 children
便可
上面也讲到 connect
是个函数,而且返回个高阶组件,因此它的基本结构为:
function connect() {
return function (WrappedComponent) {
return function (props) {
return <WrappedComponent {...props} />;
};
};
}
export default connect;
复制代码
罗列个 connect 组件要实现的功能:
store
mapStateToProps
参数,则传入 state
执行dispatch
注入组件的 props
里mapDispatchToProps
参数而且参数是个函数类型,则传入 dispatch
执行mapDispatchToProps
参数而且参数是个对象类型,则就要将参数看成 creators action
处理stateProps
,dispatchProps
,以及组件自身的 props
一并传入组件import { useContext } from "react";
import ReactReduxContext from "./context";
function connect(mapStateToProps, mapDispatchToProps) {
return function (WrappedComponent) {
return function (props) {
let stateProps = {};
let dispatchProps = {};
// 1. 接收传递下来的 store
const store = useContext(ReactReduxContext);
const { getState, dispatch } = store;
// 2. 若是传递了 mapStateToProps 参数,则传入 state 执行
if (
mapStateToProps !== "undefined" &&
typeof mapStateToProps === "function"
) {
stateProps = mapStateToProps(getState());
}
// 3. 默认将 dispatch 注入组件的 props 里
dispatchProps = { dispatch };
// 4. 若是传递了 mapDispatchToProps 参数而且参数是个函数类型,则传入 dispatch 执行
// 5. 若是传递了 mapDispatchToProps 参数而且参数是个对象类型,则就要将参数看成 creators action 处理
if (mapDispatchToProps !== "undefined") {
if (typeof mapDispatchToProps === "function") {
dispatchProps = mapDispatchToProps(dispatch);
} else if (typeof mapDispatchToProps === "object") {
dispatchProps = bindActionCreators(mapDispatchToProps, dispatch);
}
}
return <WrappedComponent {...props} {...stateProps} {...dispatchProps} />;
};
};
}
// 手写 redux 里的 bindActionCreators
function bindActionCreators(creators, dispatch) {
let obj = {};
// 遍历对象
for (let key in creators) {
obj[key] = bindActionCreator(creators[key], dispatch);
}
return obj;
}
// 将 () => ({ type:'ADD' }) creator 转成成 () => dispatch({ type:'ADD' })
function bindActionCreator(creator, dispatch) {
return (...args) => dispatch(creator(...args));
}
export default connect;
复制代码
将官方的 React-Redux 替换为手写的 provider 与 connect,能够正常显示出页面,但会发现点击按钮,页面上的值并无发生改变 在上一篇 Redux 里讲过,能够用
store.subscribe
来监听 state
的变化并执行回调。
store.subscribe(() => {
this.forceUpdate()
})
复制代码
因为 connect
是个函数组件,那么在函数里是否有相似 forceUpdate
的东西呢? 目前官方并未提供,因此只能经过模拟实现:⽤⼀个增⻓的计数器来强制从新渲染
const [, forceUpdate] = useReducer(x => x + 1, 0);
function handleClick() {
forceUpdate();
}
复制代码
在 connect
函数里加上以下代码:
const [, forceUpdate] = useReducer(x => x+1, 0)
// 之因此用 useLayoutEffect 是为了在页面渲染以前就执行,防止操做过快时,采用 useEffect 会有缺失的状况
const unsubscribe = useLayoutEffect(() => {
subscribe(()=> {
forceUpdate()
})
return () => {
if(unsubscribe) {
unsubscribe()
}
}
}, [store])
复制代码
再次验证: 能够看到点击按钮,页面已经能够即时响应了,那是否已经足够完善呢?不是的,还存在些问题,下面咱们边分析边改进
再添加个 user.js 组件:
import React, { Component } from "react";
import { connect } from "../kReactRedux";
@connect(({ user }) => ({
user,
}))
class User extends Component {
render() {
console.info(222); // 方便查看是否会从新渲染
const { user } = this.props;
return (
<div className="border"> <h3>用户信息</h3> {user.name} </div>
);
}
}
export default User;
复制代码
该组件只依赖 store
里的 user
信息,但访问该页面,会发现点击 counter
组件里的 add
按钮,会致使 user
组件一并从新渲染 这也不难理解,由于现有的代码是采用
subscribe
,一旦 store
状态更改就会触发回调,而回调里作的事情就是强制刷新,而 user
组件又是采用 connect
包装的,天然也就会从新渲染。因此应该要在触发回调时,判断下当前组件的 props
值是否更改,若是更改了才强制刷新。
要检查先后 props
的更改,就须要将上次渲染的 props
与本次渲染的 props
进行比较。而要存储上次渲染的 props
,就得采用 useRef 将上次渲染的 props
存储下来
// 6.组装最终的props
const actualProps = Object.assign({}, props, stateProps, dispatchProps);
// 7.记录上次渲染参数
const lastProps = useRef();
useLayoutEffect(() => {
lastProps.current = actualProps;
}, []);
复制代码
检测 props
是否变化是须要从新计算的,因此将获取最终 props
的逻辑抽离出来
function getProps(store, wrapperProps) {
const { getState, dispatch } = store;
let stateProps = {};
let dispatchProps = {};
// 2. 若是传递了 mapStateToProps 参数,则传入 state 执行
if (
mapStateToProps !== "undefined" &&
typeof mapStateToProps === "function"
) {
stateProps = mapStateToProps(getState());
}
console.info(stateProps, "stateProps");
// 3. 默认将 dispatch 注入组件的 props 里
dispatchProps = { dispatch };
// 4. 若是传递了 mapDispatchToProps 参数而且参数是个函数类型,则传入 dispatch 执行
// 5. 若是传递了 mapDispatchToProps 参数而且参数是个对象类型,则就要将参数看成 creators action 处理
if (mapDispatchToProps !== "undefined") {
if (typeof mapDispatchToProps === "function") {
dispatchProps = mapDispatchToProps(dispatch);
} else if (typeof mapDispatchToProps === "object") {
dispatchProps = bindActionCreators(mapDispatchToProps, dispatch);
}
}
// 6.组装最终的props
const actualProps = Object.assign(
{},
wrapperProps,
stateProps,
dispatchProps
);
return actualProps;
}
复制代码
那么要如何比较先后两个 props
是否更改呢? React-Redux
里面是采用的 shallowEqual
,也就是浅比较
// shallowEqual.js
function is(x, y) {
if (x === y) {
// 处理 +0 === -0 // true 的状况
// 当是 +0 与 -0 时,要返回 false
return x !== 0 || y !== 0 || 1 / x === 1 / y;
} else {
// 处理 NaN !== NaN // true 的状况
// 当 x 与 y 是 NaN 时,要返回 true
return x !== x && y !== y;
}
}
export default function shallowEqual(objA, objB) {
// 首先对基本数据类型的比较
// !! 如果同引用便会返回 true
if (is(objA, objB)) return true;
// 因为 is() 已经对基本数据类型作一个精确的比较,因此若是不等
// 那就是object,因此在判断两个数据有一个不是 object 或者 null 以后,就能够返回false了
if (
typeof objA !== "object" ||
objA === null ||
typeof objB !== "object" ||
objB === null
) {
return false;
}
// 过滤掉基本数据类型以后,就是对对象的比较了
// 首先拿出 key 值,对 key 的长度进行对比
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
// 长度不等直接返回false
if (keysA.length !== keysB.length) return false;
// 长度相等的状况下,进行循环比较
for (let i = 0; i < keysA.length; i++) {
// 调用 Object.prototype.hasOwnProperty 方法,判断 objB 里是否有 objA 中全部的 key
// 若是有那就判断两个 key 值所对应的 value 是否相等(采用 is 函数)
if (
!Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])
) {
return false;
}
}
return true;
}
复制代码
在 subscribe
回调里先获取最新的 props
,并与上一次的 props
进行比较,若是不同才进行更新,对应的组件就会从新渲染,而若是同样就不调用强制刷新函数,组件也就不会从新渲染。
subscribe(() => {
const newProps = getProps(store, props);
if (!shallowEqual(lastProps.current, newProps)) {
lastProps.current = actualProps;
forceUpdate();
}
});
复制代码
connect 完整代码:
import { useContext, useReducer, useLayoutEffect, useRef } from "react";
import ReactReduxContext from "./context";
import shallowEqual from "./shallowEqual";
function connect(mapStateToProps, mapDispatchToProps) {
function getProps(store, wrapperProps) {
const { getState, dispatch } = store;
let stateProps = {};
let dispatchProps = {};
// 2. 若是传递了 mapStateToProps 参数,则传入 state 执行
if (
mapStateToProps !== "undefined" &&
typeof mapStateToProps === "function"
) {
stateProps = mapStateToProps(getState());
}
// 3. 默认将 dispatch 注入组件的 props 里
dispatchProps = { dispatch };
// 4. 若是传递了 mapDispatchToProps 参数而且参数是个函数类型,则传入 dispatch 执行
// 5. 若是传递了 mapDispatchToProps 参数而且参数是个对象类型,则就要将参数看成 creators action 处理
if (mapDispatchToProps !== "undefined") {
if (typeof mapDispatchToProps === "function") {
dispatchProps = mapDispatchToProps(dispatch);
} else if (typeof mapDispatchToProps === "object") {
dispatchProps = bindActionCreators(mapDispatchToProps, dispatch);
}
}
// 6.组装最终的props
const actualProps = Object.assign(
{},
wrapperProps,
stateProps,
dispatchProps
);
return actualProps;
}
return function (WrappedComponent) {
return function (props) {
// 1. 接收传递下来的 store
const store = useContext(ReactReduxContext);
const { subscribe } = store;
const actualProps = getProps(store, props);
// 7.记录上次渲染参数
const lastProps = useRef();
const [, forceUpdate] = useReducer((x) => x + 1, 0);
const unsubscribe = useLayoutEffect(() => {
subscribe(() => {
const newProps = getProps(store, props);
if (!shallowEqual(lastProps.current, newProps)) {
lastProps.current = actualProps;
forceUpdate();
}
});
lastProps.current = actualProps;
return () => {
if (unsubscribe) {
unsubscribe();
}
};
}, [store]);
return <WrappedComponent {...actualProps} />;
};
};
}
// 手写 redux 里的 bindActionCreators
function bindActionCreators(creators, dispatch) {
let obj = {};
// 遍历对象
for (let key in creators) {
obj[key] = bindActionCreator(creators[key], dispatch);
}
return obj;
}
// 将 () => ({ type:'ADD' }) creator 转成成 () => dispatch({ type:'ADD' })
function bindActionCreator(creator, dispatch) {
return (...args) => dispatch(creator(...args));
}
export default connect;
复制代码
验证下: 点击 counter 里的
add
按钮,更改的是 count
值,因为 counter 组件里的 mapStateToProps
函数是跟 count
有关的,因此执行完 getProps
获取到的 props
跟原先的是不同的;
而 user 组件里 mapStateToProps
、mapDispatchToProps
、原有的 props
三者都与 count
无关,执行完 getProps
获取到的 props
是跟原先同样的,因此 user 组件不会从新渲染。
useSelector: 接收个函数参数,传入 state
并执行返回便可。当 state
更改时,强制从新执行
import ReactReduxContext from "./context";
import { useContext, useReducer, useLayoutEffect } from "reat";
export default function useSelector(selector) {
const store = useContext(ReactReduxContext);
const { getState, subscribe } = store;
const [, forceUpdate] = useReducer((x) => x + 1, 0);
const unsubscribe = useLayoutEffect(() => {
subscribe(() => {
forceUpdate();
});
return () => {
if (unsubscribe) {
unsubscribe();
}
};
}, []);
return selector(getState());
}
复制代码
useDispatch:
返回 dispatch 便可
import ReactReduxContext from "./context";
import { useContext } from "reat";
export default function useDispatch() {
const store = useContext(ReactReduxContext);
const { dispatch } = store;
return dispatch;
}
复制代码
React-Redux
是链接 React
和 Redux
的库,同时使用了 React
和 Redux
的API。React-Redux
提供的两个主要 api 是 Provider
与 connect
Provider
的做用是接收 store
并将它放到 contextValue
上传递下去。connect
的做用是从 store
中选取须要的属性(包括 state
与 dispatch
)传递给包裹的组件。connect
会本身判断是否须要更新,判断的依据是依赖的 store state
是否已经变化了。