我们知道,当我们的项目逐渐壮大,就需要依赖很多数据,而且数据会在各个组件中用到,所以我们就需要共享这些数据,来做到响应式的更新页面。 中文文档请看这里
Redux核心内容
action
- Redux要求我们通过action来更新数据。所有数据的变化,必须通过派发(dispatch)action来更新。
- action是一个普通的JavaScript对象,用来描述这次更新的type和value。
import {INCREMENT, DECREMENT} from './constants'
// 返回一个对象{type, value}
export const increment = num => ({
type: INCREMENT,
value: num
})
export const decrement = num => ({
type: DECREMENT,
value: num
})
reducer
- reducer是一个纯函数。将state和action联系在一起。
- reducer做的事情就是将传入的state和action结合起来生成一个新的state。
import {INCREMENT, DECREMENT} from './constants'
export default function reducer(state = {count: 0}, action) {
switch (action.type) {
case INCREMENT:
return {...state, count: state.count + action.value}
case DECREMENT:
return {...state, count: state.count - action.value}
default:
return state
}
}
store
- 提供可以访问的变量和方法。
import {createStore} from 'redux';
import reducer from './reducer'
const store = createStore(reducer);
export default store;
Redux的三大原则
单一数据源
- 整个应用程序的state被存储在一颗object tree中,并且这个object tree只存储在一个 store 中。
- Redux并没有强制让我们不能创建多个Store,但是那样做并不利于数据的维护。
- 单一的数据源可以让整个应用程序的state变得方便维护、追踪、修改。
State是只读的
- 唯一修改State的方法一定是触发action,不要试图在其他地方通过任何的方式来修改State。
- 这样就确保了View或网络请求都不能直接修改state,它们只能通过action来描述自己想要如何修改state。
- 这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心race condition(竟态)的问题。
使用纯函数来执行修改
- 通过reducer将 旧state和 actions联系在一起,并且返回一个新的State。
- 随着应用程序的复杂度增加,我们可以将reducer拆分成多个小的reducers,分别操作不同state tree的一部分。
- 但是所有的reducer都应该是纯函数,不能产生任何的副作用。 下面是redux官网提供的一幅图。
基本案例
下面我们来看一下在项目中如何使用吧。这个案例就是点击按钮,调整数字,然后响应在两个组件中。
文件结构
代码
- actionCreators.js: 定义action对象。写成函数,方便扩展。
import {INCREMENT, DECREMENT} from './constants'
// 返回一个对象{type, value}
export const increment = num => ({
type: INCREMENT,
value: num
})
export const decrement = num => ({
type: DECREMENT,
value: num
})
- constants.js: 定义action常量,防止多处使用写错。
export const INCREMENT = "increment"
export const DECREMENT = "decrement"
- index.js
import {createStore} from 'redux';
import reducer from './reducer'
const store = createStore(reducer);
export default store;
- reducer.js
import {INCREMENT, DECREMENT} from './constants'
export default function reducer(state = {count: 0}, action) {
switch (action.type) {
case INCREMENT:
return {...state, count: state.count + action.value}
case DECREMENT:
return {...state, count: state.count - action.value}
default:
return state
}
}
- 组件一
import React, { PureComponent } from 'react'
import { increment, decrement } from '../store/actionCreators.js'
import store from '../store'
export default class About extends PureComponent {
constructor(props) {
super(props)
this.state = store.getState()
}
componentDidMount() {
store.subscribe(() => {
this.setState(store.getState())
})
}
decrement(num) {
store.dispatch(decrement(num))
}
increment(num) {
store.dispatch(increment(num))
}
render() {
return (
<div>
<hr />
<div>about</div>
<div>{this.state.count}</div>
<button onClick={(e) => this.decrement(5)}>-5</button>
<button onClick={(e) => this.increment(5)}>+5</button>
</div>
)
}
}
- 组件二
import React, { PureComponent } from 'react'
import { increment, decrement } from '../store/actionCreators.js'
import store from '../store'
export default class Home extends PureComponent {
constructor(props) {
super(props)
this.state = store.getState()
}
componentDidMount() {
store.subscribe(() => {
this.setState(store.getState())
})
}
decrement(num) {
store.dispatch(decrement(num))
}
increment(num) {
store.dispatch(increment(num))
}
render() {
return (
<div>
<div>home</div>
<div>{this.state.count}</div>
<button onClick={(e) => this.decrement(5)}>-5</button>
<button onClick={(e) => this.increment(5)}>+5</button>
</div>
)
}
}
封装高阶组件
从上面的代码可以看出,有很多重复的代码逻辑,所以我么可以抽离代码,让其变的更简洁。
- mapStateToProps。高阶组件中,调用该函数并传入state。 其实我们的逻辑还是在组件中书写的,即mapStateToProps的方法体。
- mapDispatchToProps。高阶组件中,调用该函数并传入store.dispatch。 其实我们的action函数依旧是在组件中写的,只是我们在高阶组件中触发而已。
import React, { PureComponent } from 'react'
import store from '../store'
export default function StoreHOC(mapStateToProps, mapDispatchToProps) {
return (WrappedComponent) => {
class HoComponent extends PureComponent {
constructor(props) {
super(props)
this.state = store.getState()
}
componentDidMount() {
store.subscribe(() => {
this.setState(store.getState())
})
}
render() {
return (
<WrappedComponent
{...this.props}
{...mapStateToProps(this.state)}
{...mapDispatchToProps(store.dispatch)}
/>
)
}
}
return HoComponent
}
}
使用
import React, { PureComponent } from 'react'
import { increment, decrement } from '../store/actionCreators.js'
import StoreHOC from './StoreHOC'
class Home extends PureComponent {
render() {
return (
<div>
<div>home</div>
<div>{this.props.count}</div>
<button onClick={(e) => this.props.decrement(5)}>-5</button>
<button onClick={(e) => this.props.increment(5)}>+5</button>
</div>
)
}
}
const mapStateToProps = (state) => ({
...state
})
const mapDispatchToProps = (dispatch) => ({
decrement(num) {
dispatch(decrement(num))
},
increment(num) {
dispatch(increment(num))
}
})
export default StoreHOC(mapStateToProps, mapDispatchToProps)(Home)
再次优化
但是上面代码的耦合性还是挺高的,必须的引入store。如果你想让别人使用这个高阶组件,那么他们也需要在你的代码中导入store。所以就有了下面的封装。
我们可以通过React.createContext。
// context.js
import {createContext} from 'react';
const StoreContext = createContext();
export default StoreContext
高阶组件
- 就是将store换成this.context
import React, { PureComponent } from 'react'
- // import store from '../store'
import StoreContext from './context.js'
export default function StoreHOC(mapStateToProps, mapDispatchToProps) {
return (WrappedComponent) => {
class HoComponent extends PureComponent {
+ static contextType = StoreContext
+ constructor(props, context) {
super(props, context)
this.state = context.getState()
}
componentDidMount() {
- // 需要跟新state
- // store.subscribe(() => {
- // this.setState(store.getState())
- // })
+ this.context.subscribe(() => {
+ this.setState(this.context.getState())
+ })
}
render() {
return (
<WrappedComponent
{...this.props}
{...mapStateToProps(this.state)}
+ {...mapDispatchToProps(this.context.dispatch)}
/>
)
}
}
return HoComponent
}
}
使用
import StoreContext from './storeTest/context'
import store from './store'
ReactDOM.render((
<div>
<StoreContext.Provider value={store}>
<Home></Home>
<About></About>
</StoreContext.Provider>
</div>
), document.getElementById("root"))
使用react-redux
但是在工作中,我们都会使用react-redux库。
安装
npm install react-redux
使用
使用方法和上面我们自己封装的高阶函数一样。只不过我们给Provider组件设置共享数据时,是使用store,而非value。
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import store from './app/store'
import { Provider } from 'react-redux'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
redux处理异步
我们只打,当我们做项目时,需要请求数据,所以就需要发送网络请求获取数据。那么如何在redux中处理异步请求呢?
网络请求位置
- 可以放在组件的生命周期函数中。
- 可以放在redux中。
但是网络请求到的数据也属于我们状态管理的一部分,更好的应该是将其也交给redux来管理。所以我们就需要使用
redux-thunk
来完成这些操作。
redux-thunk的介绍
- 默认情况下的dispatch(action),action需要是一个JavaScript的对象。
- redux-thunk可以让dispatch(action函数),action可以是一个函数。
- 该函数会自动被调用,并且会传给这个函数一个dispatch函数和getState函数。
- dispatch函数用于我们之后再次派发action。
- getState函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态。
具体使用
我们需要redux中内置的applyMiddleware来使用redux-thunk。
import {createStore, applyMiddleware, compose} from 'redux'
import reducer from './reducer'
import thunk from 'redux-thunk'
// 这里也使用了redux-devtools工具
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) : compose
const enhancer = composeEnhancers(applyMiddleware(thunk))
const store = createStore(
reducer,
enhancer
)
export default store
在action操作中发送网络请求。
export const getBannerList = (dispatch, getState) => {
axios({
url: 'http://123.207.32.32:8000/home/multidata'
}).then((res) => {
// 二次dispatch
dispatch({
type: "getBannerList",
value: res.data.data.banner.list
})
})
}
组件中的一些逻辑
componentDidMount() {
// 调用映射后的action函数。实现一次dispatch。
this.props.getBannerList()
}
const mapDispatchToProps = (dispatch) => ({
getBannerList() {
dispatch(getBannerList)
}
})
export default connect(mapStateToProps, mapDispatchToProps)(Home)
简单剖析redux中间件
function thunk(store) {
const next = store.dispatch;
// 把它当做dispatch函数
function dispatchAndThunk(action) {
if(typeof action === "function") {
action(store.dispatch, store.getState)
}else {
next(action)
}
}
// 方式一
// store.dispatch = dispatchAndThunk
// 方式二
return dispatchAndThunk
}
function applyMiddleware(...middleware) {
for(let i = 0; i < middleware.length; i++) {
// 方式一
// middleware[i](store)
// 方式二
store.dispatch = middleware[i](store)
}
}
// 可以传入多个中间件函数
applyMiddleware(thunk)
拆分reducer
我们发现,由于项目的不断壮大,数据量越来越多,如果将所有的状态都放到一个reducer中进行管理,reducer也会变得非常臃肿。不利于维护。所以我们需要对reducer进行拆分。
如何拆分呢?
学习过vuex的同学的知道,有模块的概念。所以我们也可以将对应的数据分为一个模块,让各自维护自己的reducer, actionCreator, constants文件。然后再在总的reducer中进行合并。 那么如何在总的reducer合并呢?
这时候我们就需要用到redux提供的combineReducers函数了。
redux这部分还是比较难理解的,不会像vuex一样拿来即用。所以还需要自己好好理解redux,多多练习。