什么是redux?
Javascript 状态容器,提供可预测化的状态管理
好处:
- 可以通过redux操作对象中的属性,借助react来更新操作DOM
- 数据状态更加易于维护,能快速和准确定位到问题
redux 核心概念及数据流程
React 结合 Redux
在react 中不使用redux 遇到的问题
在react中通信的数据流是单向的,顶层组件通过props向下层组件传递数据,然而下层组件不能向上层组件传递数据,要实现下层组件修改数据,需要上层组件向下传递一个修改数据的方法,当项目变得越来越庞大时,组件之间传递数据变得越来越复杂
在react中加入 redux 的好处
使用redux ,由于Store 独立于组件, 使得数据管理独立于组件,解决了组件与组件之间传递数据的困难的问题
案例体验
环境准备
- 全局安装create-react-app
npm install create-react-app -g
- 使用create-react-app 脚手架创建项目名为react-redux-guide
create-react-app react-redux-guide
删除掉脚手架生成的多余的文件,以及删除掉现有的APP.js 和 index.js 中引用的语句,项目结构如下:
现在我们开始,实现一个计数器的案例。
//----index.js-------
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux'
// 3. 存储默认状态
const initialState = {
count: 0
}
// 2. 创建 reducer 函数
function reducer(state = initialState, action) {
if (action.type === 'increment') {
state.count = state.count + 1
} else if (action.type === 'decrement') {
state.count = state.count - 1
}
return state
}
// 1. 创建 store 对象
const store = createStore(reducer)
// 4. 定义 action
const increment = {type: 'increment'}
const decrement = {type: 'decrement'}
// 5. 定义一个计数器组件
function Counter () {
return (
<div>
<button onClick= {() => store.dispatch(decrement)} >-</button>
<span>{store.getState().count}</span>
<button onClick= {() => store.dispatch(increment)}>+</button>
</div>
)
}
store.subscribe(() => {
// console.log(store.getState())
// 重新渲染
ReactDOM.render(<Counter/>, document.getElementById('root'))
})
ReactDOM.render(
<Counter />,
document.getElementById('root')
);
实现效果:
虽然上面的代码已经实现了我们的基本功能,但是我们的代码都挤在了一个文件中,因此,我们做一个拆分。
我们在src 下新建一个文件夹component,借着创建一个组件Counter.js。项目结构如下:
Counter.js:
import React from 'react'
function Counter () {
const increment = { type: 'increment' }
const decrement = { type: 'decrement' }
return (
<div>
<button onClick={() => store.dispatch(decrement)} >-</button>
<span>{store.getState().count}</span>
<button onClick={() => store.dispatch(increment)}>+</button>
</div>
)
}
export default Counter
此时我们要如何获取store 呢?
Provider 组件 和 connect 方法
为了解决上文中无法获取到 store ,我们引入了react-redux模块中的Provider 组件和connect 方法。
Provider组件能够将store放到全局中,供其它组件调用。
connect 方法能够帮我们实现:
-
订阅store,当store中的状态发生改变时,会帮我们重新渲染组件
-
获取store中的状态,映射给组件中的props属性
-
可以获取到dispatch 方法
我们先来看看Provider组件的使用:
//--------index.js-----
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import Counter from './component/Counter'
// 3. 存储默认状态
const initialState = {
count: 0
}
// 2. 创建 reducer 函数
function reducer(state = initialState, action) {
if (action.type === 'increment') {
return {count: state.count + 1 }
} else if (action.type === 'decrement') {
return {count: state.count - 1 }
}
return state
}
// 1. 创建 store 对象
const store = createStore(reducer)
ReactDOM.render(
// 通过Provider组件,将store放到全局中去
<Provider store = { store }><Counter /></Provider>,
document.getElementById('root')
);
接着我们来看看 connect 方法:
// -------Counter.js-------
import React from 'react'
import { connect } from 'react-redux'
function Counter ({count, dispatch}) {
const increment = { type: 'increment' }
const decrement = { type: 'decrement' }
return (
<div>
<button onClick={() => dispatch(decrement)}>-</button>
<span>{count}</span>
<button onClick={() =>dispatch(increment) }>+</button>
</div>
)
}
const mapStateToProps = state => ({
count: state.count
})
export default connect(mapStateToProps)(Counter)
到此,我们还有一个问题需要处理,看下面代码
return (
<div>
<button onClick={() => dispatch(decrement)}>-</button>
<span>{count}</span>
<button onClick={() =>dispatch(increment) }>+</button>
</div>
)
button 的点击事件直接绑定了逻辑代码,显然是不易于维护的,因此我们需要进一步优化一下代码。
定义一个函数mapDispatchToProps,返回一个包含事件处理函数的对象:
const mapDispatchToProps = dispatch => ({
increment () {
dispatch({type: 'increment'})
},
decrement () {
dispatch({type: 'decrement'})
}
})
mapDispatchToProps 作为connect方法的第二参数
export default connect(mapStateToProps, mapDispatchToProps)(Counter)
更改一下函数组件
// ----Counter.js--------
import { bindActionCreators } from 'redux'
function Counter ({count, increment, decrement}) {
return (
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
)
}
bindActionCreators
const mapDispatchToProps = dispatch => ({
increment () {
dispatch({type: 'increment'})
},
decrement () {
dispatch({type: 'decrement'})
}
})
上面的代码中increment 和 decrement 方法之间重复调用了 dispatch,是属于同类代码,因此,可以优化一下:
const mapDispatchToProps = dispatch => ({
...bindActionCreators({
increment () {
return {type: 'increment'}
},
decrement () {
return {type: 'decrement'}
},
},dispatch)
})
你可能会觉得,这不是越优化,代码量变得越多了吗? 我们还没结束呢
按照下面的文件结构创建Counter.action.js
// Counter.action.js
export const increment = () => ({type: 'increment'})
export const decrement = () => ({type: 'decrement'})
在Counter.js中以别名的方式,全部导入
// Counter.js
import * as counterActions from '../store/actions/Counter.action'
const mapDispatchToProps = dispatch => bindActionCreators(counterActions,dispatch)
至此,我们通过使用bindActionCreators, 将代码优化了
拆分reducer
文件目录结构图:
// Counter.reducer.js
// 存储默认状态
const initialState = {
count: 0
}
// 创建 reducer 函数
function reducer(state = initialState, action) {
if (action.type === 'increment') {
return {count: state.count + 1 }
} else if (action.type === 'decrement') {
return {count: state.count - 1 }
}
return state
}
export default reducer
// index.js
import { createStore } from 'redux'
import reducer from './Counter.reducer'
export const store = createStore(reducer)
Redux 中间件
中间件本质上是一个函数,允许我们扩展redux的应用程序。
加入中间件后的redux的工作流程:
开发Redux中间件
模板代码
export default store => next => action => {}
我们开发一个打印日志的组件logger:
const logggerMiddleware = store => next => action => {
console.log(store)
console.log(action)
// 调用next, 执行下一个中间件或者 reducer
next(action)
}
export default logggerMiddleware
注册中间件
中间件在开发完成后,只有被注册才能生效
import { createStore, applyMiddleware } from 'redux'
import rootReducer from './reducer/Root.reducer'
import loggerMiddleware from './middleware/logger'
export const store = createStore(rootReducer, applyMiddleware(loggerMiddleware))
调用运行结果:
开发异步中间件
我们接着使用上文中计数器的案例,假设现在我们的计数器要实现延时2秒,再增加或减小。
创建一个异步中间件thunk.js:
const thunkMiddleWare = store => next => action => {
if (action.type === 'increment' || action.type === 'decrement') {
setTimeout(() => {
next(action)
}, 2000);
}
// if (typeof action === 'function') {
// return action(store.dispatch)
// }
// next(action)
}
export default thunkMiddleWare
虽然,这种方式能够达到我们的目的,但是很不灵活。假设还有另外一个需求,点击modal中的显示按钮,延迟2秒显示modal窗口。这时候我们就需要再去创建一个中间件,去实现这个功能。要是异步的功能有N 个,那我们难道就要创建N个中间件吗? 因此,上述方法不适用,我们需要另寻它法。
要解决上面遇到的问题,我们需要思考一下如何实现一个灵活通用的中间件:
- 当前这个中间件函数不关心你想执行什么样的异步操作 只关心你执行的是不是异步操作
- 如果你执行的是异步操作 你在触发action的时候 给我传递一个函数 如果执行的是同步操作 就传递action对象
- 异步操作代码要写在你传递进来的函数中
- 当前这个中间件函数在调用你传递进来的函数时 要将dispatch方法传递过去
按照上面的思路,我们更改一下thunk.js代码:
const thunkMiddleWare = store => next => action => {
if (typeof action === 'function') {
return action(store.dispatch)
}
next(action)
}
增加一个处理异步增加的函数:
// Counter.action.js
export const increment_async = payload => dispatch => {
setTimeout(() => {
dispatch(increment(payload))
}, 2000)
}
常用redux 中间件
redux-thunk
使用步骤:
- npm install redux-thunk
- import thunk from 'redux-thunk'
- 注册 applyMiddleware
export const store = createStore(RootReducer, applyMiddleware(thunk));
redux-saga
redux-saga 解决的问题: 可以将异步操作从Action Creator 文件中抽离出来, 放在一个单独的文件中。
redux-saga 的使用:
- 下载
npm install redux-saga
- 创建redux-saga 中间件
import createSageMiddleware from 'redux-saga'
// 创建sagaMiddleWare中间件
const sagaMiddleWare = createSageMiddleware()
- 注册sagaMiddleSaga
createStore(rootReducer,
// 注册中间件
applyMiddleware(sagaMiddleWare)
)
- 使用saga接受action执行异步操作
import { takeEvery, put, delay } from 'redux-saga/effects';
import { increment } from '../actions/counter.actions';
import { INCREMENT_ASYNC } from '../const/counter.const';
// takeEvery 接收 action
// put 触发 action
function* increment_async_fn (action) {
yield delay(2000);
yield put(increment(action.payload))
}
export default function* counterSaga () {
// 接收action
yield takeEvery(INCREMENT_ASYNC, increment_async_fn)
}
- 启动saga
//
// 启动saga
sagaMiddleWare.run(counterSaga)
all 合并多个saga
结合上文中的案例,我们现在要处理modal 的异步显示,处理过程:
- 创建一个modal.saga.js,代码如下:
// modal.saga.js
import { takeEvery, put, delay } from 'redux-saga/effects'
import { SHOW_MODAL_ASYNC } from '../constant/Counter.constant'
import { show } from '../actions/Modal.action'
function* show_modal_fn () {
yield delay(2000)
yield put(show())
}
export default function * modalSaga () {
yield takeEvery(SHOW_MODAL_ASYNC, show_modal_fn)
}
- 创建 root.saga.js
import { all } from 'redux-saga/effects'
import counterSaga from './counter.saga'
import modalSaga from './modal.saga'
export default function * rootSaga() {
yield all([
counterSaga(),
modalSaga()
])
}
- 启动 rootSaga
sagaMiddleWare.run(rootSaga)
redux-action
redux-action 解决的问题:
redux 流程中大量的样板代码读写很痛苦,使用redux-action 可以简化Action 和 reducer 的处理
redux-action 的使用:
- 下载
npm install redux-action
- 创建Action
import { createAction } from 'redux-actions'
// 创建Action
export const increment = createAction('increment')
export const decrement = createAction('decrement')
- 创建reducer
import { handleActions as createReducer } from 'redux-actions'
import { decrement, increment } from '../actions/counter.createAction'
// 存储默认状态
const initialState = {
count: 0
}
const handleIncrement = (state) => {
return {
count: state.count + 1
}
}
const handleDecrement = (state) => {
return {
count: state.count - 1
}
}
export default createReducer({
[increment]: handleIncrement,
[decrement]: handleDecrement
}, initialState)