Redux 使用
前言:
- javascript 开发的应用程序,已经变得越来越复杂了
- javascript需要管理的状态越来越多,越来越复杂
- 这些状态包括服务器返回的数据,缓存数据,用户操作产生的数据等等,也包括一些UI的状态,比如某些元素是否被选中,是否显示加载动效,当前分页
- 管理不断变化的state是非常困难的
- 状态之间会相互存在依赖,一个状态的变化会引起另一个状态的变化, view页面也会引起状态的变化
- 当应用程序复杂的时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变的非常难以控制和追踪
- react是在视图层帮助我们呢解决了DOM的渲染过程,但是state依然是留给我们自己管理的
- 无论是组件定义自己的state,还是组件之间的通信通过props进行传递,也包括通过context进行数据之间的共享
- react主要负责帮助我们管理视图,state如何维护最终还是我们自己来决定的
- Redux就是一个帮助我们管理State的容器“Redux是 Javascript的状态容器,提供了可预测的状态管理
- Readux除了和React一起使用之外,它也可以和其他界面库一起使用(Vue), 它非常小,包括依赖也只有2kb
Redux核心理念 -store
- redux的核心理念非常简单
- 比如我们呢有一个朋友列表需要管理
- 如果我们呢没有定义同意的规范来操作这段数据,那么整个数据变化就是无法跟踪的
- 比如页面的某处通过products.push的方式增加了一条数据
- 比如另一个页面通过 products[0].age = 25 修改了一条数据
- 整个应用程序错综复杂,当出现bug时,很难跟踪到底哪里发生的变化
Redux核心理念 -action
- redux要求我们通过action来更新数据
- 所有数据的变化,必须通过派发(dispatch)action来更新
- action是一个普通的javascript对象,用来描述这次更新的type和content
- 比如下面就是几个更新 friends 和 action
- 强制使用action的好处是可以清晰的知道数据到底发生了什么样的变化,所有的数据变化都是可以追踪的,可预测的
- 当然,目前我们的action是固定的对象,真实的应用中,我们会通过函数来定义,返回一个action
const action1 = {type:"ADD_FRIEND",info:{name:"lucy",age:20}}
Redux核心理念 -reducer
- 但是如何将state 和action 联系在一起呢?答案是 reducer
- reducer是一个纯函数
- reducer做的事情就是传入的state和action结合起来生成一个新的state
Redux 的 三大原则
- 单一数据源
- 整个程序的state被存储在一个object tree中,并且这个object tree只存储在一个store中
- Redux并没有强制让我们不能创建多个Store,但是那样做不利于数据的维护
- 单一的数据源可以让整个应用程序的state变得方便维护,追踪,修改
- state是只读的
- 唯一修改State的方法一定 是出发action ,不要试图在其他地方通过action来描述自己想要如何修改state
- 这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心 race condition(竞态)的问题
- 使用纯函数来执行
- 通过reducer 将旧state和action联系在一起,并且返回一个新的State
- 随着应用程序的复杂度增加,我们可以将reducer拆分成多个小的reducer,分别操作不同的state tree的一部分
- 但是所有的reducer都应该是纯函数,不能产生任何的副作用
Redux基础使用
const redux = require('redux')
const initialState = {
counter: 0
}
// 1.store(创建的时候传入一个 reducer)
const store = redux.createStore(reducer)
// 2.action
const action1 = { type: "INCREMENT" }
const action2 = { type: "DECREMENT" }
const action3 = { type: "ADD_NUMBER", number: 5 }
const action4 = { type: "SUB_NUMBER", number: 12 }
// 3.订阅store的修改
store.subscribe(() => {
console.log("state 发生了改变", store.getState().counter);
})
// 4.派发action
store.dispatch(action1)
store.dispatch(action2)
store.dispatch(action2)
store.dispatch(action3)
store.dispatch(action4)
// 5.定义reducer
function reducer (state = initialState, action) {
switch (action.type) {
case "INCREMENT":
return { ...state, counter: state.counter + 1 }
case "DECREMENT":
return { ...state, counter: state.counter - 1 }
case "ADD_NUMBER":
return { ...state, counter: state.counter + action.number }
case "SUB_NUMBER":
return { ...state, counter: state.counter - action.number }
default:
return state
}
}
redux融入react代码
// Home页面
import React, { PureComponent } from 'react';
import store from '../store';
import {
addAction
} from '../store/actionCreators';
export default class Home extends PureComponent {
constructor(props) {
super(props);
this.state = {
counter: store.getState().counter
}
}
componentDidMount() {
store.subscribe(() => {
this.setState({
counter: store.getState().counter
})
})
}
render() {
return (
<div>
<h1>Home</h1>
<h2>当前计数: {this.state.counter}</h2>
<button onClick={e => this.increment()}>+1</button>
<button onClick={e => this.addCounter()}>+5</button>
</div>
)
}
increment() {
store.dispatch(addAction(1));
}
addCounter() {
store.dispatch(addAction(5));
}
}
// Profile页面
import React, { PureComponent } from 'react';
import store from '../store';
import {
subAction
} from '../store/actionCreators';
export default class Profile extends PureComponent {
constructor(props) {
super(props);
this.state = {
counter: store.getState().counter
}
}
componentDidMount() {
store.subscribe(() => {
this.setState({
counter: store.getState().counter
})
})
}
render() {
return (
<div>
<hr/>
<h1>Profile</h1>
<div>
<h2>当前计数: {this.state.counter}</h2>
<button onClick={e => this.decrement()}>-1</button>
<button onClick={e => this.subCounter()}>-5</button>
</div>
</div>
)
}
decrement() {
store.dispatch(subAction(1));
}
subCounter() {
store.dispatch(subAction(5));
}
}
上面的代码其实非常简单,核心代码主要是两个:
- 在
componentDidMount
中定义数据的变化,当数据发生变化时重新设置counter
; - 在发生点击事件时,调用store的
dispatch
来派发对应的action
;
自定义connect函数
上面的代码是否可以实现react组件
和redux
结合起来呢?
- 当然是可以的,但是我们会发现每个使用的地方其实会有一些重复的代码:
- 比如监听store数据改变的代码,都需要在
componentDidMount
中完成; - 比如派发事件,我们都需要去先拿到
store
, 在调用其dispatch
等;
能否将这些公共的内容提取出来呢?
我们来定义一个connect函数:
-
这个connect函数本身接受两个参数:
-
- 参数一:里面存放
component
希望使用到的State
属性; - 参数二:里面存放
component
希望使用到的dispatch
动作;
- 参数一:里面存放
-
这个connect函数有一个返回值,是一个高阶组件:
-
- 在
constructor
中的state中保存一下我们需要获取的状态; - 在
componentDidMount
中订阅store中数据的变化,并且执行setState
操作; - 在
componentWillUnmount
中需要取消订阅; - 在
render
函数中返回传入的WrappedComponent
,并且将所有的状态映射到其props
中; - 这个高阶组件接受一个组件作为参数,返回一个class组件;
- 在这个class组件中,我们进行如下操作
- 在
-
import React, { PureComponent } from "react"; import store from '../store'; export default function connect(mapStateToProps, mapDispatchToProps) { return function handleMapCpn(WrappedComponent) { return class extends PureComponent { constructor(props) { super(props); this.state = { storeState: mapStateToProps(store.getState()) } } componentDidMount() { this.unsubscribe = store.subscribe(() => { this.setState({ storeState: mapStateToProps(store.getState()) }) }) } componentWillUnmount() { this.unsubscribe(); } render() { return <WrappedComponent {...this.props} {...mapStateToProps(store.getState())} {...mapDispatchToProps(store.dispatch)}/> } } } }
-
在home和props文件中,我们按照自己需要的state、dispatch来进行映射:
-
比如home.js中进行如下修改:
-
-
mapStateToProps:用于将state映射到一个对象中,对象中包含我们需要的属性;
-
mapDispatchToProps:用于将dispatch映射到对象中,对象中包含在组件中可能操作的函数;
-
- 当调用该函数时,本质上其实是调用dispatch(对应的Action);
-
const mapStateToProps = state => {
return {
counter: state.counter
}
}
const mapDispatchToProps = dispatch => {
return {
addNumber: function(number) {
dispatch(addAction(number));
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Home);
context 处理store
- 但是上面的connect 函数有一个很大的缺陷:依赖导入的store
- 如果我们将其封装成一个独立的库,需要依赖创建的store,我们应该如何去获取
- 肯定不能让用户更改我们的源码
- 正确的做法是我们提供一个Provider Provider 来自我们创建的Context,让用户将store传入value中
react-redux使用
- redux和 react没有直接的关系,你可以将redux用在其他的框架里面
- 话虽如此,但是redux 确实是和 react , Deku 结合非常好的库,因为他们是通过state函数来描述界面的状态,redux可以发射状态的更新,让他们做出相应的变化
- 虽然我们之前已经实现了 connect Provider这些帮助我们完成链接redux react 的辅助工具,但是实际上redux官方帮助我们提供了react-redux的库,可以直接在项目中使用,并且实现的逻辑会更加的严谨和高效
- 安装react-redux
- yarn install react-redux
组件中的异步操作(一)
- 在之前的简单案例中,redux中保存的counter是一个本地定义的数据
- 我们可以直接通过同步操作来 dispatch actoin ,state就会立即更新
- 但是真实开发中,redux中保存的很多数据可能来自服务器,我们需要进行异步的请求,再将数据保存到redux中
- 在之前学习的网络请求的时候我们讲过,网络请求可以在class类组件的componentDidMount中发送
-
组件中的异步操作(二)
-
上面的代码有一个缺陷
- 我们必须将网络请求的异步代码放到组件的生命周期中来完成
- 事实上,网络请求到的数据也属于我们状态管理的一部分,更好的一种方式应该是将其也交给redux来管理;
-
但是在 redux中如何进行异步操作呢
- 答案就是使用 中间件
- 学习过 express 或者 koa框架的话 对中间件一定印象
- 在这类框架中 middleware 可以帮助我们在请求和响应之间嵌入一些操作的代码,比如cookie 解析,日志记录,文件压缩等操作
理解中间件
- redux也引入了中间件(Middleware)的概念
- 这个中间件的目的是在dispatch的action和最终达到的reducer之间,扩展一些自己的代码;
- 比如日志记录、调用异步接口、添加代码调试功能等等;
- 我们现在要做的事情就是发送异步的网络请求,所以我们可以添加对应的中间件:
- 这里官网推荐的、包括演示的网络请求的中间件是使用 redux-thunk
- redux-thunk是如何做到让我们可以发送异步的请求呢?
- 我们知道,默认情况下的dispatch(action),action需要是一个JavaScript的对象;
- redux-thunk可以让dispatch(action函数),action可以是一个函数;
- 该函数会被调用,并且会传给这个函数一个dispatch函数和getState函数
- dispatch函数用于我们之后再次派发action;
- getState函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态;
如何使用redux-thunk
- 安装 yarn add redux-thunk
- 在创建store时传入应用了middleware的enhance函数
- 通过applyMiddleware来结合多个Middleware, 返回一个enhancer
- 将enhancer作为第二个参数传入到createStore中;
- 定义返回一个函数的action:
- 注意:这里不是返回一个对象了,而是一个函数;
- 该函数在dispatch之后会被执行;
redux-devtools
- 我们之前讲过,redux可以方便的让我们对状态进行跟踪和调试,那么如何做到呢
- redux官网为我们提供了redux-devtools的工具
- 利用这个工具,我们呢可以知道每次状态是如何被修改的
- 安装该工具需要两个步骤
- 第一步:在对应的浏览器中安装相关的插件
- 第二步:在redux中集成devtools的中间件
generator的使用
- saga中间件使用了es6的generator语法
- 我们安装如下步骤演示一下生成器的使用过程
- javascript 中编写一个普通的函数,进行调用会立即拿到这个函数的结果
- 如果我们将这个函数编写成一个生成器函数
- 调用iterator的next函数,会销毁一次迭代器,并且返回一个yield的结果
- 研究一下foo生成器函数代码的执行顺序
- gennerator和promise一起使用
redux-saga的使用
- redux-saga是另一个常用的redux发送异步请求的中间件,它的使用更加灵活
- redux-saga的使用步骤如下
- 1.安装 redux-saga
- yarn add redux-saga
- 2.集成redux-saga中间件
- 导入创建中间件的函数
- 通过创建中间件的函数,创建中间件,并且放到applyMiddleware函数中
- 启动中间件的监听过程,并且传入要监听的saga
- 3.saga.js文件的编写
- takeEvery:可以传入多个监听的actio Type,每一个都可以被执行(对应有一个takeLatest,会取消前面的)
- put:在saga中派发action不再是通过dispatch,而是通过put
- all:keyi zai yield的使用put多个action
打印日志需求
- 前面我们已经提过,中间件的目的是在redux中插入一些自己的操作:
- 比如我们现在有一个需求,在dispatch之前,打印一下本次的action对象,dispatch完成之后可以打印一下最新的store ,state
- 也就是我们需要将对应的代码插入到redux的某部分,让之后所有的dispatch都可以包含这样的操作;
- 如果没有中间 件,我们是否可以实现类似的代码呢? 可以在派发的前后进行相关的打印
- 但是这种方式缺陷非常明显
- 首先,每一次的dispatch操作,我们都需要在前面加上这样的逻辑代码
- 其次,存在大量重复的代码,会非常麻烦和臃肿
- 是否有一种更优雅的方式来处理这样的相同逻辑呢?
- 我们可以将代码封装到一个独立的函数中
- 但是这样的代码有一个非常大的缺陷:
- 调用者(使用者)在使用我的dispatch时,必须使用我另外封装的一个函数dispatchAndLog
- 显然,对于调用者来说,很难记住这样的API,更加习惯的方式是直接调用dispatch;
修改dispatch
-
事实上,我们可以利用一个hack一点的技术:Monkey Patching,利用它可以修改原有的程序逻辑
-
我们对代码进行如下的修改
- 这样就意味着我们已经直接修改了dispatch的调用过程
- 在调用dispatch的过程中,真正调用的函数其实是dispatchAndLog
-
当然,我们可以将它封装到一个模块中,只要调用这个模块中的函数,就可以对store进行这样的处理
-
function patchLogging(store) { let next = store.dispatch; function dispatchAndLog(action) { console.log("dispatching:", action); next(addAction(5)); console. log("新的state: ", store.getState()); } store.dispatch = dispatchAndLog; }
thunk需求
-
redux-thunk的作用:
- 我们知道redux中利用一个中间件redux-thunk可以让我们的dispatch不再只是处理对象,并且可以处理函数
- 那么redux-thunk中的基本实现过程是怎么样的呢?事实上非常的简单
-
我们来看下面的代码
-
我们又对dispatch进行转换,这个dispatch会判断传入的
function patchThunk(store) { let next = store.dispatch; function dispatchAndThunk(action){ if (typeof action =zs "function") { action(store.dispatch, store.getState); } else { next(action); } } store.dispatch = dispatchAndThunk; }
-
合并中间件
- 单个调用某个函数来合并中间件并不是特别的方便,我们可以封装一个函数来实现所有的中间件合并:
function applyMiddleware(store,middlewares) {
middlewares = middlewares.slice();
middlewares.forEach(middleware => {
store.dispatch = middleware(store)
})
}
applyMiddleware(store,[patchLogging,patchThunk])
我们来理解一下上面操作之后,代码的流程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KaypnGUz-1606021308680)(C:\Users\kim\AppData\Roaming\Typora\typora-user-images\image-20201005204652597.png)]
当然,真实的中间件实现起来会更加的灵活,这里我们仅仅做一个抛砖引玉,有兴趣可以参考redux合并中间件的源码流程
Reducer代码拆分
- 我们先来理解一下,为什么这个函数叫reducer?
- 我们来看一下目前我们的reducer
- 当前这个reducer既有处理counter的代码,又有处理home页面的数据
- 后续counter相关的状态或home相关的状态会进一步变得更加复杂
- 我们也会继续添加其他的相关状态,比如购物车、分类、歌单等等;
- 如果将所有的状态都放到一个reducer中进行管理,随着项目的日趋庞大,必然会造成代码臃肿、难以维护。
- 因此,我们可以对reducer进行拆分:
- 我们先抽取一个对counter处理的reducer;
- 再抽取一个对home处理的reducer;
- 将它们合并起来;
Reducer文件拆分
-
目前我们已经将不同的状态处理拆分到不同的reducer中,我们来思考
- 虽然已经放到不同的函数了,但是这些函数的处理依然是在同一个文件中,代码非常的混乱;
- 另外关于reducer中用到的constant、action等我们也依然是在同一个文件中;
combineReducers函数
- 目前我们合并的方式是通过每次调用reducer函数自己来返回一个新的对象。
- 事实上,redux给我们提供了一个combineReducers函数可以方便的让我们对多个reducer进行合并:
const reducer = combineReducers({
counterInfo:counterReducer,
homeInfo:homeReducer
})
export default reducer
- 那么combineReducers是如何实现的呢
- 事实上,它也是讲我们传入的reducers合并到一个对象中,最终返回一个combination的函数(相当于我们之前的reducer函 数了);
- 在执行combination函数的过程中,它会通过判断前后返回的数据是否相同来决定返回之前的state还是新的state;
- 新的state会触发订阅者发生对应的刷新,而旧的state可以有效的组织订阅者发生刷新;