常用链接
redux 是 js 应用的可预测状态的容器。 可以理解为全局数据状态管理工具(状态管理机),用来做组件通信等。
React Redux 官方网址: https://react-redux.js.org/
中文文档:https://cn.redux.js.org
https://github.com/reduxjs/redux
https://github.com/reduxjs/react-redux
https://github.com/reduxjs/redux-thunk
简单举例说明
一个学校有4套“春夏秋冬”校服,学生怎么知道什么时候该穿什么样的校服?
- 保安亭(store) 里面放着保安(reducer)、里面窗户上挂着衣服(state)
- 同学们(订阅者) 只有看服装的权限,没有改服装的权限(安全)
- 校长(发布者) 派发动作“改衣服”给保安,告诉他要换“秋装”了;
- 保安接收后,就把挂着的衣服换成“秋装”,同时通知同学们;
- 同学们就去看现在的服装是什么样子,然后第2天,就穿着秋装来上学了。
Redux 应用场景
- 随着 Javascript 单页面应用开发日趋复杂,管理不断变化的 state 非常困难。
- Redux 的出现就是为了解决 state 里的数据问题。
- 在 React 中,数据在组件中是单向流动的。
- 数据从一个方向父组件流向子组件(通过props),由于这个特征,两个非父子关系的组件之间(兄弟组件)的通信就比较麻烦。
Redux 设计思想
- Redux 是将整个应用状态存储到一个地方,称为 store。
- 里面保存一颗状态数 state tree。
- 组件可以派发 dispatch 行为 action 给 store,而不是直接通过其它组件。
- 其它组件可以通过订阅 store 中的状态(state),来刷新自己的视图。
Redux 三大原则
- 整个应用的 state 被存储在一颗 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
- State 是只读的,唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象,使用纯函数来执行修改,为了描述 action 如何改变 state tree,需要编写 reducers。
- 单一数据源的设计让 React 的组件之间的通信更加方便,同时也便于状态的统一管理。
安装
cnpm i -S redux react-redux
cnpm i -D redux-devtools # 开发者工具
redux
createStore 创建store仓库
createStore(reducer, initState)
- 第一个参数:处理器;第二个参数:状态的初始值
- https://github.com/reduxjs/redux/blob/master/src/createStore.ts
原生计数器
public/index.html
<body>
<div id="root"></div>
<div id="counter">
<p id="count-value">0</p>
<button id="add-btn">add</button>
<button id="minus-btn">minus</button>
</div>
</body>
src/index.js
import { createStore } from 'redux';
let countValue = document.querySelector('#count-value');
let addBtn = document.querySelector('#add-btn');
let minusBtn = document.querySelector('#minus-btn');
let initState = {number: 0};
/** reducer 处理器
*
* @param {*} state 老状态
* @param {*} action 动作,里面一般有2参数:type(类型)、payload(额外参数)
* @returns 新状态
*/
const reducer = (state=initState, action) => {
switch (action.type){ //根据不同动作类型,进行其各自的处理
case 'add':
// return {number: state.number + action.payload};
return {number: state.number + 1};
case 'minus':
return {number: state.number - 1};
default:
return state;
}
}
// 创建store仓库(第一个参数:处理器;第二个参数:状态的初始值)
// let store = createStore(reducer, initState);
let store = createStore(reducer);
function render(){
let state = store.getState();
countValue.innerHTML = JSON.stringify(state);
}
render(); //初始渲染一次
// 订阅 store.subscribe (该函数没有参数)
store.subscribe((...args) => {
console.log('args=>', args); // args=> []
render();
})
addBtn.addEventListener('click', () => {
// store.dispatch({type: 'add', payload: 5}); // 派发action
store.dispatch({type: 'add'});
})
minusBtn.addEventListener('click', () => {
store.dispatch({type: 'minus'});
})
React 计数器
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import Counter1 from './components/Counter1'
ReactDOM.render(<Counter1 />, document.getElementById('root'));
src/components/Counter1.js
import React from 'react';
import {createStore} from 'redux';
const reducer = (state=initState, action) => {
switch (action.type){
case 'add':
return {number: state.number + action.payload};
case 'minus':
return {number: state.number - 1};
default:
return state;
}
}
let initState = {number: 10};
let store = createStore(reducer, initState);
class Counter1 extends React.Component{
state={number: store.getState().number};
componentDidMount(){
this.unsubscribe = store.subscribe(() => {
this.setState({number: store.getState().number});
})
}
componentWillUnmount(){
this.unsubscribe();
}
add = () => {
store.dispatch({type: 'add', payload: 5});
}
minus = () => {
store.dispatch({type: 'minus'});
}
render (){
return (
<div>
<p>{this.state.number}</p>
<button onClick={this.add}>add</button>
<button onClick={this.minus}>minus</button>
</div>
)
}
}
export default Counter1;
bindActionCreators 绑定action创建者
https://github.com/reduxjs/redux/blob/master/src/bindActionCreators.ts
src/components/Counter1.js
import React from 'react';
+ import {createStore, bindActionCreators} from 'redux';
const reducer = (state=initState, action) => {
switch (action.type){
case 'add':
return {number: state.number + 1};
case 'minus':
return {number: state.number - 1};
default:
return state;
}
}
let initState = {number: 10};
let store = createStore(reducer, initState);
+ // 创建 action
+ const add = () => ({type: 'add'});
+ const minus = () => ({type: 'minus'});
+ // 创建一个 actionCreator 对象
+ const actions = {add, minus};
+ // 绑定 actionCreator
+ // 绑定之后 bindActions.add 就变成了 () => store.dispatch({type: 'add'})
+ const bindActions = bindActionCreators(actions, store.dispatch);
class Counter1 extends React.Component{
state={number: store.getState().number};
componentDidMount(){
this.unsubscribe = store.subscribe(() => {
this.setState({number: store.getState().number});
})
}
componentWillUnmount(){
this.unsubscribe();
}
render (){
return (
<div>
<p>{this.state.number}</p>
+ <button onClick={bindActions.add}>add</button>
+ <button onClick={bindActions.minus}>minus</button>
</div>
)
}
}
export default Counter1;
combineReducers
https://github.com/reduxjs/redux/blob/master/src/combineReducers.ts
- redux应用中,reducer、store、state都只有一个,都是单例的。
- 如果应用比较庞大的话,肯定要写很多 reducer,然后再合并成一个 reducer。
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import Counter1 from './components/Counter1'
import Counter2 from './components/Counter2'
ReactDOM.render(
<div>
<Counter1 />
<Counter2 />
</div>,
document.getElementById('root')
);
src/store/index.js 仓库
import {createStore} from 'redux';
import rootReducer from './reducers';
const store = createStore(rootReducer); //rootReducer 就是根reducer
export default store;
src/store/action-types 动作类型
export const ADD1 = 'ADD1';
export const MINUS1 = 'MINUS1';
export const ADD2 = 'ADD2';
export const MINUS2 = 'MINUS2';
export const ALLMINUS = 'ALLMINUS';
src/store/reduces 合并所有的分reducer
src/store/reducers/index.js
import {combineReducers} from 'redux';
import counter1 from './counter1';
import counter2 from './counter2';
// 合并所有的分 reducer
let reducers = {counter1, counter2};
let rootReducer = combineReducers(reducers);
export default rootReducer;
src/store/reducers/counter1.js
import * as types from '../action-types.js';
// 这是 Counter1 组件对应的分 reducer,它有自己独立的状态
const counter1 = (state={number:5}, action) => {
switch (action.type){
case types.ADD1:
return {number: state.number + 1};
case types.MINUS1:
return {number: state.number - 1};
case types.ALLMINUS:
return {number: state.number - 1};
default:
return state;
}
}
export default counter1;
src/store/reducers/counter2.js
import * as types from '../action-types.js';
// 这是 Counter2 组件对应的分 reducer,它有自己独立的状态
const counter2 = (state={number:10}, action) => {
switch (action.type){
case types.ADD2:
return {number: state.number + 1};
case types.MINUS2:
return {number: state.number - 1};
case types.ALLMINUS:
return {number: state.number - 1};
default:
return state;
}
}
export default counter2;
src/store/actions 动作对象
src/store/actions/counter1.js
import * as types from '../action-types';
const actions = {
add1: () => ({type: types.ADD1}),
minus1: () => ({type: types.MINUS1}),
}
export default actions;
src/store/actions/counter2.js
import * as types from '../action-types';
const actions = {
add2: () => ({type: types.ADD2}),
minus2: () => ({type: types.MINUS2}),
allMinus: () => ({type: types.ALLMINUS}),
}
export default actions;
src/components
可以看出组件里还是有很多重复的逻辑处理,这就需要用 react-redux 的connect 高阶组件去优化处理。
src/components/Counter1.js
import React from 'react';
import {bindActionCreators} from 'redux';
+ import store from '../store';
+ import actions from '../store/actions/counter1';
const bindActions = bindActionCreators(actions, store.dispatch);
class Counter1 extends React.Component{
+ state={number: store.getState().counter1.number};
componentDidMount(){
this.unsubscribe = store.subscribe(() => {
+ this.setState({number: store.getState().counter1.number});
})
}
componentWillUnmount(){
this.unsubscribe();
}
render (){
return (
<div>
<p>{this.state.number}</p>
+ <button onClick={bindActions.add1}>add1</button>
+ <button onClick={bindActions.minus1}>minus1</button>
</div>
)
}
}
export default Counter1;
src/components/Counter2.js
import React from 'react';
import {bindActionCreators} from 'redux';
+ import store from '../store';
+ import actions from '../store/actions/counter2';
const bindActions = bindActionCreators(actions, store.dispatch);
class Counter2 extends React.Component{
+ state={number: store.getState().counter2.number};
componentDidMount(){
this.unsubscribe = store.subscribe(() => {
+ this.setState({number: store.getState().counter2.number});
})
}
componentWillUnmount(){
this.unsubscribe();
}
render (){
return (
<div>
<p>{this.state.number}</p>
+ <button onClick={bindActions.add2}>add2</button>
+ <button onClick={bindActions.minus2}>minus2</button>
+ <button onClick={bindActions.allMinus}>allMinus</button>
</div>
)
}
}
export default Counter2;
react-redux
Provider
- https://github.com/reduxjs/react-redux/blob/master/src/components/Provider.js
- 其实就是 react-redux 的 context 上下文。
- 接收一个 store 属性并向下层组件传递。
src/index.js
```jsx import React from ‘react’; import ReactDOM from ‘react-dom’; import {Provider} from ‘react-redux’; import store from ‘./store’; import Counter1 from ‘./components/Counter1’ import Counter2 from ‘./components/Counter2’
ReactDOM.render(
<a name="qHSeZ"></a>
## connect
- [https://github.com/reduxjs/react-redux/tree/master/src/connect](https://github.com/reduxjs/react-redux/tree/master/src/connect)
- 负责把仓库和组件进行关联
<a name="dklzG"></a>
##### src/components/Counter1.js
```jsx
import React from 'react';
import {connect} from 'react-redux';
import actions from '../store/actions/counter1';
import * as types from '../store/action-types';
class Counter1 extends React.Component{
render (){
let ps = this.props;
return (
<div>
<p>{ps.number}</p>
<button onClick={ps.add1}>add1</button>
<button onClick={ps.minus1}>minus1</button>
</div>
)
}
}
// 语法:connect(mapStateToProps, mapDispatchToProps, ...)
// 第一个参数 mapStateToProps:状态映射函数。
// 接收仓库的总状态,返回指定的分状态, connect再把函数返回值映射为组件属性
function mapStateToProps(state){
return state.counter1;
}
// 第二个参数 mapDispatchToProps:主要把经过 bingActionCreators 绑定后的 actions对象 映射为属性。
// 大致有7中写法
// 第1种,传函数。接收 store.dispatch 为参数,返回绑定后的 actions对象
function mapDispatchToProps(dispatch){
return {
add1: () => dispatch({type: types.ADD1}),
minus1: () => dispatch({type: types.MINUS1}),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Counter1);
// 第2种:直接传未经过绑定的 actions对象
// export default connect(mapStateToProps, actions)(Counter1);
src/components/Counter2.js
import React from 'react';
import {connect} from 'react-redux';
import actions from '../store/actions/counter2';
class Counter2 extends React.Component{
render (){
let ps = this.props;
return (
<div>
<p>{ps.number}</p>
<button onClick={ps.add2}>add2</button>
<button onClick={ps.minus2}>minus2</button>
<button onClick={ps.allMinus}>allMinus</button>
</div>
)
}
}
function mapStateToProps({counter2}){
return counter2;
}
export default connect(mapStateToProps, actions)(Counter2);
hooks (useDispatch、useSelector、useState)
新版的react-redux为了更好的用于函数组件,提供了一些 hooks。
- https://github.com/reduxjs/react-redux/tree/master/src/hooks
useDispatch
获取 store.dispatchuseSelector
状态映射函数 mapStateToProps(state => currentComponentState)useStore
获取仓库 store
src/components/Counter1.js
import React from 'react';
import {useDispatch, useSelector, useStore} from 'react-redux';
import * as types from '../store/action-types';
function Counter1(){
let dispatch = useDispatch(); //store.dispatch
let state = useSelector(state => state.counter1); //获取状态
let store = useStore(); //获取仓库 {dispatch, getState, subscribe}
return (
<div>
<p>{state.number}</p>
<button onClick={() => dispatch({type: types.ADD1})}>add1</button>
<button onClick={() => dispatch({type: types.MINUS1})}>minus1</button>
</div>
)
}
export default Counter1;
applyMiddleware(redux中间件)
简述
- 如果没有中间件的运用,redux的工作流程就是
action -> reducer
,这就相当于是同步操作,由dispatch触发action后,直接去reducer执行相应的动作。 - 但是在某些比较复杂的业务逻辑中,这种同步的实现方式并不能很好的解决问题。比如有一个这样的需求:点击按钮 -> 获取服务器数据 -> 渲染视图,因为获取服务器数据是需要异步实现,所以这时就需要引入中间件改变redux同步执行的流程,形成异步流程来实现所要的逻辑,有了中间件,redux的工作流程就变成:action -> middlewares -> reducer,点击按钮就相当于dispatch触发action,接下去获取服务器数据middlewares的执行,当middlewares成功获取到服务器数据就去触发reducer对应的动作,更新需要渲染视图的数据。
- 中间件的机制可以让我们改变数据流,实现异步action,action过滤,日志输出,异常报告等功能。
日志中间件
- 实现一个日志中间件,在更改状态时,打印前后的状态。
- 直接通过改写 dispatch 方法,实现在更改状态时,打印前后的状态。
- 但这种方案并不好
实现日志
src/index.js
import store from './store';
store.subscribe(() => console.log('state =>', store.getState()));
store.dispatch({type: 'ADD1'});
src/store/index.js
import {createStore} from 'redux';
import rootReducer from './reducers';
const store = createStore(rootReducer);
// 先获取原生的dispatch方法
let dispatch = store.dispatch;
// 再改写 dispatch 方法,实现在更改状态时,打印前后的状态
store.dispatch = function(action){
console.log('prev state', store.getState());
dispatch(action);
console.log('next state', store.getState());
return action;
}
export default store;
实现异步
import {createStore} from 'redux';
import rootReducer from './reducers';
const store = createStore(rootReducer);
let dispatch = store.dispatch;
store.dispatch = function(action){
setTimeout(() => {
dispatch(action);
}, 3000);
return action;
}
export default store;
单个日志中间件
- 实现一个真正的日志中间件
src/index.js
```jsx import store from ‘./store’; store.subscribe(() => console.log(‘state =>’, store.getState()));
// 派发普通对象 store.dispatch({type: ‘ADD1’});
// 派发函数 // store.dispatch((dispatch, getState) => { // setTimeout(() => { // dispatch({type: ‘ADD1’}); //这里可以继续派发函数 // }, 3000) // });
// 派发promise // store.dispatch(new Promise((resolve, reject) => { // setTimeout(() => { // resolve({type: ‘ADD1’}); // }, 3000) // }));
<a name="nxzlX"></a>
##### src/store/index.js
```jsx
import {createStore} from 'redux';
import rootReducer from './reducers';
function applyMiddleware(middleware){
return function(createStore){
return function(reducer){
let store = createStore(reducer); //先创建原生的store
let dispatch; //指向改造后的dispatch方法
let middlewareAPI = {
getState: store.getState,
dispatch: action => dispatch(action),
}
dispatch = middleware(middlewareAPI)(store.dispatch); //改写dispatch方法
return {...store, dispatch};
}
}
}
// 日志中间件的真正实现(只要是中间件,格式都是定死的)
function logger(store){
return function(next){
return function(action){
console.log('prev state', store.getState());
next(action);
console.log('next state', store.getState());
}
}
}
const store = applyMiddleware(logger)(createStore)(rootReducer);
export default store;
单个支持派发函数、promise的中间件
支持函数
// 原生的 store.dispatch 只支持传递普通对象,不能接受函数,也不能接收promise。
// 为了要让其支持,添加 支持函数的中间件
function thunk(store){
return function(next){
return function(action){
// 如果派发的action是一个函数,就让它执行
if (typeof action === 'function'){
// 如果这里第一个参数传递next(即原生的store.dispatch方法),
// 那么函数内 dispatch 如果继续派发函数,那就报错了。
return action(store.dispatch, store.getState);
}
// 如果不是函数,则意味着不需要自己处理。直接调用下一个dispatch方法即可。
return next(action);
}
}
}
const store = applyMiddleware(thunk)(createStore)(rootReducer);
支持promise
// 添加支持promise的中间件
function promise(store){
return function(next){
return function(action){
if (typeof action.then === 'function'){
return action.then(newAction => store.dispatch(newAction));
}
return next(action);
}
}
}
const store = applyMiddleware(promise)(createStore)(rootReducer);
级联中间件
- 上面都是单一的中间件,但是真正项目里假如有很多中间件,怎么办呢?
- 在真实的项目中,中间件每个逻辑都是单独编写的,但是可以向 applyMiddleware传递多个中间件
const store = applyMiddleware(promise, thunk, logger)(createStore)(rootReducer);
function applyMiddleware(...middlewares){
return function(createStore){
return function(reducer){
let store = createStore(reducer); //先创建原生的store
let dispatch; //指向改造后的dispatch方法
let middlewareAPI = {
getState: store.getState,
dispatch: action => dispatch(action),
}
// dispatch = middleware(middlewareAPI)(store.dispatch); //改写dispatch方法
let chain = middlewares.map(item => item(middlewareAPI));
let [promise, thunk, logger] = chain;
dispatch = promise(thunk(logger(store.dispatch)));
return {...store, dispatch};
}
}
}
const store = applyMiddleware(promise, thunk, logger)(createStore)(rootReducer);
看起来,是不是有点洋葱模型的感觉了,这里就引出来了 compose 管道。
compose、applyMiddleware 的具体实现见下一节 redux-源码篇。
示例
src/store/index.js
import {createStore, applyMiddleware} from 'redux';
import reduxLogger from 'redux-logger';
import reduxThunk from 'redux-thunk';
import reduxPromise from 'redux-promise';
import rootReducer from './reducers';
const store = applyMiddleware(
reduxPromise,
reduxThunk,
reduxLogger
)(createStore)(rootReducer);
export default store;
src/components/Couter1.js
import React from 'react';
import {useDispatch, useSelector, useStore} from 'react-redux';
import * as types from '../store/action-types';
function Counter1(){
let dispatch = useDispatch(); //store.dispatch
let state = useSelector(state => state.counter1); //获取状态
let store = useStore(); //获取仓库 {dispatch, getState, subscribe}
return (
<div>
<p>{state.number}</p>
{/* 派发参数为 普通对象 */}
<button onClick={() => dispatch({type: types.ADD1})}>add1</button>
<button onClick={() => dispatch({type: types.MINUS1})}>minus1</button>
{/* 派发参数为 函数 */}
<button onClick={() => dispatch((resDispatch, resGetState) => {
setTimeout(() => {
resDispatch({type: types.ADD1});
}, 2000)
})}>function +</button>
{/* 派发参数为 Promise */}
<button onClick={() => dispatch(new Promise((resolve) => {
setTimeout(() => {
resolve({type: types.MINUS1});
}, 2000)
}))}>Promise -</button>
</div>
)
}
export default Counter1;