深入浅出react-redux和redux源码
概述
现在前端框架普遍以组件为基本组成单位,react也不例外,所以学习react组件之间怎么传递数据就显得非常重要,我们先来了解一下组件中普通的传递数据的方式,再来编写redux和react-redux源码,一步步分析redux是怎么传递数据的。
普通传递数据的方式:props和context
众所周知,redux实现了全局数据分享,能够在多个组件之间共享数据,但是redux并不是专门为react准备的,在react中使用redux会稍显麻烦,所以我们可以使用react-redux来以一种更优雅的方式实现redux中的发布订阅设计模式
props和context是基本的传递数据的方式,react-redux也是基于这两者实现的,并且配合redux共享数据,我们会用几个小案例剖析props和context是怎么传递数据的,流程可能会稍显冗长,但是对于我们接下来分析react-redux和redux源码是有很大的帮助的。
props属性传递
这应该是最常用的的一种方式了,调取子组件的时候,把信息基于属性的方式传递给子组件(子组件PROPS中存储传递的信息),这种方式只能父组件把信息传递子组件,子组件无法直接的把信息传递给父组件,也就是属性传递信息是单向传递的。
简单的代码示例:
import React from "react";
export default class Panel extends React.Component {
constructor(props) {
super(props);
this.state = { counter: 0 };
}
//可以在组件上通过添加属性的方式传递props
render() {
return (
<section className="Panel">
<h1>Panel</h1>
<Head counter={this.state.counter} />
<Body add={this.add} minus={this.minus} />
</section>
);
}
add = () => {
this.setState({ counter: this.state.counter + 1 });
};
minus = () => {
this.setState({ counter: this.state.counter - 1 });
};
}
// 子组件Head
class Head extends React.Component {
render() {
return (
<section className="Head">
<h2>Head</h2>
<p>{this.props.counter}</p>
</section>
);
}
}
// 子组件Body
class Body extends React.Component {
render() {
return (
<section className="Body">
<h2>Body</h2>
<button onClick={this.props.add}>add</button>
<button onClick={this.props.minus}>minus</button>
</section>
);
}
}
实际效果:
head中的属性,传递进来了counter值:
Body中的属性,传递进来了add和minus方法:
context上下文传递
父组件先把需要给后代元素(包括孙子元素)使用的信息都设置好(设置在上下文中),后代组件需要用到父组件中的信息,主动去父组件中调取使用即可。
- 新版本的context
新版本的React context使用了Provider和Customer模式,和react-redux的模式非常像。在顶层的Provider中传入value,在子孙级的Consumer中获取该值,并且能够传递函数,用来修改context。 代码示例:
import React, { useContext } from "react";
const Context = React.createContext();
const Provider = Context.Provider;
const Consumer = Context.Consumer;
export default class Panel extends React.Component {
constructor(props) {
super(props);
this.state = { counter: 0 };
}
render() {
return (
<section className="Panel">
<h1>Panel</h1>
<Provider
value={{
counter: this.state.counter,
add: this.add,
minus: this.minus,
}}
>
<Head />
<Consumer>{(ctx) => <Body ctx={ctx} />}</Consumer>
</Provider>
</section>
);
}
add = () => {
this.setState({ counter: this.state.counter + 1 });
};
minus = () => {
this.setState({ counter: this.state.counter - 1 });
};
}
// 子组件Head
function Head() {
const store = useContext(Context);
let { counter } = store;
console.log(counter);
return (
<section className="Head">
<h2>Head</h2>
<p>{counter}</p>
</section>
);
}
// 子组件Body
class Body extends React.Component {
render() {
const { add, minus } = this.props.ctx;
return (
<section className="Body">
<h2>Body</h2>
<button onClick={add}>add</button>
<button onClick={minus}>minus</button>
</section>
);
}
}
Provider中的值,有我们需要的属性:
Consumer中的值,将props转变为contenxt,并通过回调函数,返回子组件,并将context的值放到子组件的props属性上:
子组件中的值,有props属性:
可以看出子组件是通过获取Consumer中的context的内容,并转换为自己的props属性
- 老版本的context
- childContextTypes: 根组件中声明,指定context的结构类型,不声明,会报错;
- getChildContext: 根组件中声明的一个函数,返回一个对象,就是context;
- contextTypes 子孙组件中声明,指定要接收的context的结构类型,可以只是context的一部分结构,contextTypes 没有定义,context将是一个空对象;
- this.context 在子孙组件中通过此来获取上下文。
注:从React v15.5开始 ,React.PropTypes 助手函数已被弃用,可使用 prop-types 库 来定义contextTypes,通过“yarn add prop-types”安装即可。
代码示例:
import React from "react";
import PropTypes from "prop-types";
export default class Panel extends React.Component {
// 父组件设置信息
static childContextTypes = {
// 设置上下文中信息值的类型
counter: PropTypes.number,
add: PropTypes.func,
minus: PropTypes.func,
};
getChildContext() {
// RETURN的是啥,相当于往上下文中放了啥
return {
counter: this.state.counter,
add: this.add,
minus: this.minus,
};
}
constructor(props) {
super(props);
this.state = { counter: 0 };
}
render() {
return (
<section className="Panel">
<h1>Panel</h1>
<Head />
<Body />
</section>
);
}
add = () => {
this.setState({ counter: this.state.counter + 1 });
};
minus = () => {
this.setState({ counter: this.state.counter - 1 });
};
}
// 子组件Head
class Head extends React.Component {
// 子组件主动获取需要的信息
static contextTypes = {
// 首先类型需要和设置时候类型一样,否则报错,只需要写你需要用到的。
counter: PropTypes.number,
};
render() {
console.log(this.context);
return (
<section className="Head">
<h2>Head</h2>
<p>{this.context.counter}</p>
</section>
);
}
}
// 子组件Body
class Body extends React.Component {
static contextTypes = {
add: PropTypes.func,
minus: PropTypes.func,
};
render() {
return (
<section className="Body">
<h2>Body</h2>
<button onClick={this.context.add}>add</button>
<button onClick={this.context.minus}>minus</button>
</section>
);
}
}
父组件的信息:
子组件的信息,通过contextTypes获取需要的信息:
属性 VS 上下文
- 属性操作起来简单,子组件是被动接收传递的值(组件内的属性是只读的),只能父传子(子传父不行,父传孙也需要处理:父传子,子再传孙);
- 上下文操作起来相对复杂一些,子组件是主动获取信息使用的(子组件是可以修改获取到的上下文信息的,但是不会影响到父组件中的信息,其它组件不受影响),一旦父组件设置了上下文信息,它后代组件都可以直接拿来用,不需要一层层的传递
redux
概述:
redux可以应用在任何的项目中(VUE/JQ/RERACT的都可以),react-redux才是专门给react项目提供的方案。
基本流程:
- createStore 创建store
- reducer 初始化、修改状态函数
- getState 获取状态值
- dispatch 提交更新
- subscribe 变更订阅
redux里面的主要方法
- createStore:创建一个store,存储的是各个组件的状态信息,当你传进来的是一个reducer函数,redux里面会先执行一次dispatch,用内部的一个变量state接收reducer函数默认执行的初始值,createStore是典型的发布订阅设计模式,它返回一个对象,包含dispatch,getState,subscribe等方法,并通过闭包保存了两个值,分别是state和listenAry,state保存了最新的状态信息,listenAry保存了subscribe发布的任务。通过这三个方法可以形成一个闭环,保证在更新数据后,重新渲染组件,渲染组件时拿到新的数据
- combineReducers:能够组合多个reducer函数,每个reducer函数代表项目中一部分组件的功能,combineReducers参数是多个reducer函数组成的对象,它执行后返回一个函数,这个函数作为新的reducer函数传入createStore,新的reducer执行后会返回一个对象,由以前的单个reducer函数的函数名/函数执行结果组成键值对,描述起来比较绕,但是等一下写完源代码,便会一目了然;
- applyMiddleware:是一个增强器,可以提供各种中间件来增强redux的功能,比如logger提供了日志记录功能,thunk提供了实现异步任务的功能,因为入redux只是个纯粹的状态管理器,默认只支持同步,实现异步任务,比如延迟,网络请求,需要thunk。它是一个典型的柯里化函数,接收多个函数作为参数,返回一个新的函数。
现在我们将上面的案例通过redux进行改造,在改造之前,我们先用一张简单的图表来描述整个redux工作流程
代码示例:
import React from "react";
import { createStore, combineReducers, applyMiddleware } from "redux";
import logger from "redux-logger";
import thunk from "redux-thunk";
// 这是一个典型的reducer,通过匹配不同的action来返回不同的值
function counterReducer(state = 0, action) {
switch (action.type) {
case "add":
return state + 1;
case "minus":
return state - 1;
default:
return state;
}
}
const store = createStore(
combineReducers({ counterReducer }),
applyMiddleware(logger, thunk)
);
export default class ReduxPage extends React.Component {
render() {
return (
<section className="Panel">
<h1>ReduxPage</h1>
<Head />
<Body />
</section>
);
}
}
// 子组件Head
class Head extends React.Component {
componentDidMount() {
// subscribe函数是把一个新的方法增加到事件池中,通常是获取最新的新的状态信息,从而重新渲染组件
store.subscribe(() => {
// 虽然在这里通过setState获取的是一个空对象,但是setState会调用forceUpdate方法,forceUpdate再调用render方法重新进行渲染。
this.setState({});
});
}
render() {
return (
<section className="Head">
<h2>Head</h2>
//组件渲染时在这里通过store里面的getState方法拿到新的数据并显示出来。
<p>counter:{store.getState().counterReducer}</p>
</section>
);
}
}
// 子组件Body
class Body extends React.Component {
render() {
return (
<section className="Body">
<h2>Body</h2>
<button onClick={this.add}>add</button>
<button onClick={this.minus}>minus</button>
<button onClick={this.asyAdd}>asyAdd</button>
</section>
);
}
add = () => {
// 派发新的任务,传入新的state和action,我们不用传入state,因为数据简单,只需要操纵原来的数据,可以通过action告诉reducer怎么操纵原来的数据,并通过createStore里面的state接收reducer新返回的数据,在通过getState拿到新的数据。
store.dispatch({ type: "add" });
};
minus = () => {
store.dispatch({ type: "minus" });
};
asyAdd = () =>
store.dispatch((dispatch) => {
setTimeout(() => {
dispatch({ type: "add" });
}, 1000);
});
}
通过以上动图可以看到同步任务和异步任务都可以顺利执行。
logger也显示出了日志。
子组件和父组件上面没有任何值,说明是通过redux在进行全局数据管理。
- 在这个项目中,我们通过getState方法拿到初始值,并在组件首次渲染时将初始值渲染在组件上;
- 通过dispatch派发了一个任务,dispatch方法里面传入了一个action对象,reducer根据action执行返回一个新的值,通过createStore里面的state来接收这个值;
- 然后执行listenAry这个数组里面保存的所有方法,而里面的方法是通过subcribe添加进来的,subcribe添加的方法主要是获取最新的组件状态信息,获取会调用组建的render方法,组件会重新渲染;
- 渲染后在通过getState方法拿到createStore里面的最新的state值,并将最新的state值和组件一起渲染到页面上,这就形成了一个闭环。
通过原生JS实现redux,logger和thunk
疑惑
相信你在看完以上的代码后任然会有许多疑问,为什么createStore执行后所暴露出的方法会形成一个闭环,combineReducers是怎么样实现多个子reducer的聚合的,applyMiddleware是怎么实现增强器功能的,logger是怎样实现日志输出的,thunk又是怎么样实现异步任务功能的。接下来我们通过JS来实现以上功能。
原生JS实现redux
export function createStore(reducer, enhancer) {
//=>创建一个STORE,STATE用来存储管理的状态信息,listenAry用来存储事件池中的方法
//=>STATE不用设置初始值,因为第一次DISPATCH执行REDUCER,STATE没有值,走的是REDUCER中赋值的默认值信息,我们自己会在创建容器的时候就把DISPATCH执行一次!
//enhancer存在并且是函数的话就执行它。
if (enhancer && typeof enhancer === 'function') {
return enhancer(createStore)(reducer);
}
let currentState = undefined;
let listenAry = [];
//=>DISPATCH:基于DISPATCH实现任务派发
function dispatch(action) {
//1.执行REDUCER,修改容器中的状态信息(接收REDUCER的返回值,把返回的信息替换原有的STATE),值得注意的是:我们是把返回值全部替换STATE,所有要求REDUCER中在修改状态之前,要先把原始的状态信息克隆一份,在进行单个的属性修改
currentState = reducer(currentState, action);
for (let i = 0; i < listenAry.length; i++) {
let item = listenAry[i];
if (typeof item === "function") {
item();
} else {
listenAry.splice(i, 1);
i--;
}
}
// listenAry.map((cl) => cl());
}
dispatch({ type: "?INIT_DEFAULT_STATE" });
function getState() {
// 我们需要保证返回的状态信息不能和容器中的STATE是同一个堆内存(否则外面获取状态信息后,直接就可以修改容器中的状态了,这不符合DISPATCH->REDUCER才能改状态的规范)
return JSON.parse(JSON.stringify(currentState)); // 深度克隆对象
}
//=>SUBSCRIBE:向事件池中追加方法
function subscribe(listener) {
//1.向容器中追加方法(重复验证)
let isExit = listenAry.includes(listener);
if (!isExit) {
listenAry.push(listener);
}
// 返回一个方法:执行返回的方法会把当前绑定的方法在事件池中移除掉
return function unsubscribe() {
let index = listenAry.indexOf(listener);
// listenAry.splice(index, 1);//=>可能会引发数组塌陷
listenAry[index] = null;
};
}
return {
dispatch,
getState,
subscribe,
};
}
export function combineReducers(reducers) {
// REDUCERS:传递进来的REDUCER对象集合
return function reducer(state = {}, action) {
// DISPATCH派发执行的时候,执行的是返回的REDUCER,这里也要返回一个最终的STATE对象替换原有的STATE,而且这个STATE中包含每个模块的状态信息{counterReducer:...}
// 我们所谓的REDUCER合并,其实就是DISPATCH派发的时候,把每一个模块的REDUCER都单独执行一遍,把每个模块返回的状态最后汇总在一起,替换容器中的状态信息
let newState = {};
for (let key in reducers) {
if (!reducers.hasOwnProperty(key)) break;
// reducers[key]:每个模块单独的REDUCER
// state[key]:当前模块在REDUX容器中存储的状态信息
// 返回值是当前模块最新的状态,把它在放到NEW-STATE中
newState[key] = reducers[key](state[key], action);
}
return newState;
};
}
// 在createStore中执行applyMiddleware(...middleWares)返回applyMiddle函数,并通过middleWares在数组中保存了中间件,比如logger,thunk
export function applyMiddleware(...middleWares) {
// 在createStore内部执行后,返回一个匿名函数,并通过createStore这个形参保存了最新的createStore函数。
return function applyMiddle(createStore) {
// 在createStore内部再次执行这个匿名函数,保存了reducer函数
return (...arg) => {
// 在内部通过createStore(...arg)执行了createStore(reducer),返回了一个对象,保存有dispatch,getState,subscribe等方法,并通过闭包保存了两个值,分别是state和listenAry
const store = createStore(...arg);
const midApi = {
getState: store.getState,
dispatch: store.dispatch,
};
// 通过middleWare数组的map方法返回了中间件执行后的函数
const chain = middleWares.map((mw) => mw(midApi));
// 通过compose方法中的reduce将两个中间件函数折叠为一个,从前向后执行参数为store.dispatch
const dispatch = compose(...chain)(store.dispatch);
// 将两个中间件函数折叠为一个函数再作为新的dispatch,将它作为新的对象的一部分返回给外部的store
return {
...store,
dispatch,
};
};
};
}
function compose(...funcs) {
if (funcs.length === 0) {
return () => {
console.log("empty function");
};
} else if (funcs.length === 1) {
return funcs[0];
} else {
return funcs.reduce((left, right) => (...args) => right(left(...args)));
}
}
特别注意
在原生的createStore方法里面,subscribe方法设计有缺陷会导致数组塌陷问题 以下代码:
return function unsubscribe() {
let index = listenAry.indexOf(listener);
listenAry.splice(index, 1);// 可能会引发数组塌陷
};
subscribe执行后会返回一个unsubscribe函数,这个函数通过记录添加在任务数组中的当前函数的位置,执行后可以删除当前添加的函数,但是它是立即删除,假设一下,如果我们添加了3个方法,其中第二个方法包含有删除第一个方法的代码,第二个方法执行后会立即在数组中删除第一个方法,这会导致数组中的元素整体前移一位,第三个方法的索引变到了第二个位置,但是数组还是会执行第三个索引上的函数,会漏过原先的函数,我们因该避免这件事情发生。
解决方案:
return function unsubscribe() {
let index = listenAry.indexOf(listener);
// listenAry.splice(index, 1);// 可能会引发数组塌陷
// 先将要删除的方法置为空,再在便利的时候,删除值为空的那一项,再将索引减一
listenAry[index] = null;
};
// 在dispatch中遍历的时候,删除值为空的那一项,再将索引减一
for (let i = 0; i < listenAry.length; i++) {
let item = listenAry[i];
if (typeof item === "function") {
item();
} else {
listenAry.splice(i, 1);
i--;
}
}
下面再来实现logger和thunk函数
//在logger参数里面将midApi解构出来,拿到其中的getState方法
export function logger({ getState }) {
// 将store.dispatch或者其他的函数传入进去,但是其他函数也会包含store.dispatch函数
// 将最里层的箭头函数返回。
return (dispatch) => (action = {}) => {
let oldValue = getState();
dispatch(action);
let newValue = getState();
console.log(
action.type + "执行了" + "执行时间是 " + new Date().toLocaleTimeString()
);
console.log("旧值:", oldValue);
console.log("action: ", action);
console.log("新值:", newValue);
console.log("-------------分割线-------------");
};
}
// 在applyMiddleware里面执行这一函数,返回里面的箭头函数
export function thunk() {
// 将store.dispatch或者其他的函数传入进去,但是其他函数也会包含store.dispatch函数
// 将最里层的箭头函数返回。
return (dispatch) => (action) => {
if (typeof action === "function") {
return action(dispatch);
}
dispatch(action);
};
}
现在将项目中的redux,logger和thunk替换为我们自己写的再来试一下,看是否能用
import React from "react";
// 我们自己的redux在KRedux文件里面,logger和thunk在MyReduxStore文件里面,看一下效果。
import { createStore, combineReducers, applyMiddleware } from "../../KRedux";
import { logger, thunk } from "../../store/MyReduxStore";
function counterReducer(state = 0, action) {
switch (action.type) {
case "add":
return state + 1;
case "minus":
return state - 1;
default:
return state;
}
}
const store = createStore(
combineReducers({ counterReducer }),
applyMiddleware(thunk, logger)
);
// 3个class组件我就不写了,和上面一模一样。
动态图,和上面的没有任何区别,怀疑代码是否有效的读者可以copy我的代码,自己尝试一下,在我的电脑上是没有问题的:
我自己写的logger记录器:
不能捕获异步信息,实在能力有限,望读者见谅
组件内容信息:
父组件和子组件上没有任何props属性和contenxt信息,说明全部是基于redux进行数据管控。
手写react-redux源码
接下来我们自己实现react-redux源码,它可以帮我们自动发布获取最新状态信息的任务,更简便的实现redux中的发布订阅。
react-redux中重要的方法有两个:provider和connect
- Provider 为后代组件提供store;
- connect 为组件提供数据和变更方法。
react-redux代码:
import React, { useState, useContext, useEffect } from "react";
const Context = React.createContext();
// PROVIDER:当前项目的“根”组件
// 1. 接收通过属性传递进来的STORE,把STORE挂载到上下文中,这样当前项目中任何一个组件中,想要使用REDUX中的STORE,直接通过上下文获取即可
// 2. 在组件的RENDER中,把传递给PROVIDER的子元素渲染
export const Provider = (props) => {
return (
// 使用Context.Provider把store放到上下文中,并渲染子元素
<Context.Provider value={props.store}>{props.children}</Context.Provider>
);
};
//CONNECT:高阶组件,返回函数组件,也可以用class组将实现
export const connect = function (
// mapStateToProps:回调函数,把REDUX中的部分状态信息挂载到指定组件的属性上
// 把RETURN的对象挂载到属性上
mapStateToProps = (state) => state,
// 对象或者回调函数,把一些需要派发的任务方法也挂载到组件的属性上,返回的方法中有执行dispatch派发任务的操作
mapDispatchToProps
) {
// connectMid
// 参数:传递进来的是要操作的组件,我们需要把指定的属性和方法都挂载到当前组件的属性上
// 返回值
// 1.返回一个新的组件Proxy(代理组件),在代理组件中,我们要获取Provider在上下文中存储的store。
// 2.紧接着获取store中的state和dispatch,把mapStateToProps回调函数执行、mapDispatchToProps对象值拿到。
// 3.接收返回的结果,把这些结果挂载到Component这个要操作组件的属性上
// 4.把传递进来的Cmp组件渲染到页面上
return function connectMid(Cmp) {
return function Proxy() {
const store = useContext(Context);
function getProps() {
// 在当前示例中,stateProps值为{counter: {counterReducer: 0}}
const stateProps = mapStateToProps(store.getState());
// bindActionCreators方法很重要,它将我们传递进来的方法和store.dispatch绑定在一起
const dispatchProps =
mapDispatchToProps instanceof Object
? bindActionCreators(mapDispatchToProps, store.dispatch)
: mapDispatchToProps instanceof Function
? mapDispatchToProps(store.dispatch)
: null;
// 返回一个新对象,包含stateProps值和dispatchProps对象
return {
...stateProps,
...dispatchProps,
};
}
// 在组件渲染完后通过store的subscribe方法发布一个setProps方法,可以获取最新的store信息,并再次触发渲染
useEffect(() => {
store.subscribe(() => {
setProps({ ...getProps() });
});
});
// useState设置调用getProps()初始化state值,并设置setProps获取最新的store信息
const [props, setProps] = useState({ ...getProps() });
return <Cmp {...props} />;
};
};
};
// 传递进来两个值分别为传递进来的方法对象和store.dispatch方法
function bindActionCreators(creators, dispatch) {
// Object.keys(creators)获取方法对象键值组成的数组,
// 通过这个数组的reduce方法生成一个对象ret,对象键为:数组里面的值,值为store.dispatch(creators[item](...args)),
return Object.keys(creators).reduce((ret, item) => {
ret[item] = bindActionCreator(creators[item], dispatch);
// 将对象返回,通过dispatchProps接收
return ret;
}, {});
}
// 生成一个新方法,内容为store.dispatch(creators[item]())。
// 就是store.dispatch方法参数是creators[item](...args)
function bindActionCreator(creator, dispatch) {
return (...args) => dispatch(creator(...args));
}
代码实例:
store文件,redux还是有点小问题,单独使用,实现异步调用,配合react-redux使用,异步会出现死递归的情况,有能力的人可以改一下我的代码
import thunk from "redux-thunk";
import logger from "redux-logger";
import { createStore, combineReducers, applyMiddleware } from "../KRedux";
// import { createStore, combineReducers, applyMiddleware } from "redux";
function counterReducer(state = 0, action) {
switch (action.type) {
case "add":
return state + 1;
case "minus":
return state - 1;
default:
return state;
}
}
const store = createStore(
combineReducers({ counterReducer }),
applyMiddleware(logger, thunk)
);
export default store;
index文件:
import { Provider } from "./KReact-redux";
import store from "./store/MyReactReduxStore";
import {
HeadPage,
BodyPage,
} from "./reduxPage/reduxDemo/MyReactReduxPage";
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.Fragment>
<Provider store={store}>
<div className="Panel">
<h1>MyReactReduxPage</h1>
<HeadPage />
<BodyPage />
</div>
</Provider>
</React.Fragment>,
rootElement
);
MyReactReduxPage文件:
import React from "react";
import { connect } from "../../KReact-redux";
class Head extends React.Component {
render() {
const { counter } = this.props;
return (
<section className="Head">
<h1>HeadPage</h1>
<p>counter:{counter.counterReducer}</p>
</section>
);
}
}
export const HeadPage = connect((state) => {
return { counter: state };
}, null)(Head);
// 子组件Body
class Body extends React.Component {
render() {
const { add, minus, asyAdd } = this.props;
return (
<section className="Body">
<h2>Body</h2>
<button onClick={add}>add</button>
<button onClick={minus}>minus</button>
<button onClick={asyAdd}>点击</button>
</section>
);
}
}
export const BodyPage = connect(null, {
add: () => {
return { type: "add" };
},
minus: () => {
return { type: "minus" };
},
asyAdd: () => {
return (dispatch) => {
setTimeout(() => {
dispatch({ type: "add" });
}, 1000);
};
},
})(Body);
整体组件结构:
Provider组件
Proxy代理组件:
两个子组件:
后记
到这里,redux及相关的源码整体复现完毕,虽然还是有一点小问题,但是帮助我们理解整个redux流程还是很有帮助的,虽然现在新项目中大都使用hooks,但是redux还是有很重要的作用的,了解redux对于我们的工作,有很积极的意义。