一、什么是redux
redux是js状态容器,主要提供状态管理,它可以运行于服务器、客户端、原生应用;它除了支持react外,还支持其他的ui框架,体积小只有2kb;
redux不是必须搭配react才能使用,除了支持react以外他还支持其他的ui框架,之所以我把它放到react章节下去讲是因为我们经常在大的react项目中经常见到redux(我也是为此才去了解的react)
二、学习redux之前,先讲一下我对redux的认识
1、redux的使用场景
当你的react项目中需要共享的任务状态很多时,组件的数据处理过于繁杂时,redux才会发挥其作用,使用redux可以将数据和ui分离,但是鲁迅说过:“如果你不知道是否需要 Redux,那就是不需要它”,总之,不要把 redux 当作万灵丹,如果你的应用没那么复杂,就没必要用它。另一方面,Redux 只是 Web 架构的一种解决方案,也可以选择其他方案。
2、react中的state 和 redux中的state是完全不一样的东西
react的state 和 redux的state 根本是完全不沾边的两个东西,切记不要混为一谈。react的state是存在于类组件中的一个存放自身变量的属性,redux的state整个应用的store中的某一个namespace下的状态。二者没有任何关系;
3、redux是独立于react的
假如您的应用中要使用redux,您是需要将react和redux关联起来的,这里有一个包叫react-redux,是用来关联react和redux的,其中有一个高阶函数叫connect,它接受两个方法,mapStateToProps和mapDispatchToProps是这两个方法将redux的state注入到了react的props中;
4、redux的编程思维太繁琐了
在初步学习完redux后,我的第一反应就是,这个redux也太繁琐了吧
Redux 带来了函数式编程、不可变性思想等等,为了配合这些理念,开发者必须要写很多“模式代码(boilerplate)”,繁琐以及重复是开发者不愿意容忍的。当然也有很多 hack 旨在减少 boilerplate,但目前阶段,可以说 Redux 天生就附着繁琐;
在react项目中如果用不好反而让自己的项目看上去更繁杂,所以我的看法是能不用就不用了,如果你想在react中用上任务状态管理器,mobx 更是一个不错的选择(毕竟连redux的作者也推荐了mobx),编程体验相比redux简直不要太舒服;
三、redux的基本概念
redux的原则:
- 唯一数据源
- 保持只读状态
- 数据改变只能通过函数来执行
下面我们将开始redux的基本概念学习
Store
Store是用于存储数据的容器,整个应用只有一个Store,Redux提供了一个createStore
方法,用来创建一个store
import { createStore } from 'redux';
const store = createStore(fn);
上面示例中createStore
函数接受另一个函数
作为参数,返回新生成的 Store 对象。
至于作为参数的函数要怎么设计,我们后面会说;
ps:
createStore
方法可以不止一个函数reducer
参数哦,它还可以传整个应用的初始状态preloadedState
作为第二个参数,增强器enhancer
例如 applyMiddleware() 作为第三个参数createStore(reducer, preloadedState, enhancer)
State
我前面也说了这里的state你可不能和react的state
混在一起,这两个可不是一个概念,store包含了所有的数据,而state是store中某个时间的数据集合,你可以理解为store某时刻生成的快照,而生成快照的方法是store.getState()
import { createStore } from 'redux';
const store = createStore(fn);
const state = store.getState();
Redux 规定, 一个 State 对应一个 View。只要 State 相同,View 就相同。你知道 State,就知道 View 是什么样,反之亦然。
View是啥?state是某一时刻数据的快照,那view就是对应state某一时刻的ui视图(个人理解)
Action
store中数据是只读状态,不能直接去改数据,如果你需要改数据的话只能通过,redux提供的方法去执行
Action
Action就是我们发出的通知,表示state可能要发生变化了,他是一个对象,这个对象必须带上type
属性,表示Action的名称,其他属性可以自由的去设置
const action = {
type: 'ADD_TODO',
payload: 'Learn Redux'
};
上面代码中,Action 的名称是ADD_TODO,它携带的信息是字符串Learn Redux。
Action Creator
View 要发送多少种消息,就会有多少种 Action。如果都手写,会很麻烦。可以定义一个函数来生成 Action,这个函数就叫 Action Creator。
const ADD_TODO = '添加 TODO';
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
const action = addTodo('Learn Redux');
上面代码中,addTodo函数就是一个 Action Creator。
其实一开始到这里时为我也很困惑Action Creator存在的意义,我写一个Action Creator函数只是为了生成一Action,那我为什么不自己跳过Action Creator函数这个步骤,直接写action对象就好,后来我想明白了,在项目中这个action可能会用到很多次,每次都写一个action对象好麻烦,不如封装一个函数来用,困惑就接触了,也就明白了Action Creator的意义
store.dispatch
action是一个对象,你只写一个action描述,显然没用啊,你还要发它发出去给到store,我们就需要在ui组件中使用store.dispatch()
把发出 Action 发出去,store.dispatch()
是 View 发出 Action 的唯一方法。
/* 状态管理容器中定义 */
import { createStore } from 'redux';
const store = createStore(fn);
/* ui容器中去引用 store.dispatch 发出修改state的通知 */
store.dispatch({
type: 'ADD_TODO',
payload: 'Learn Redux'
});
// 你看什么action对象很多方法都去调用多麻烦,结合Action Creator你可以写成
store.dispatch(addTodo('Learn Redux'));
Reducer(重点,容易头晕)
前面我们我们讲过 createStore
方法需要传一个函数类型的参数,这个函数我们要怎么写?就是我们要讲的reducer
当store接收到了ui那边dispatch
方法方法中的action
之后怎么去得到一个新的state
,就取决于你reducer函数怎么写;
Reducer 是一个函数,它接受 当前 State 和 Action 作为参数,返回一个新的 State。
const reducer = function (state, action) {
// ...在这里面一般是结合state、action去定义一个new_state,
return new_state;
};
整个应用的初始状态,可以作为 State 的默认值。下面是一个实际的例子。
const defaultState = 0;
const reducer = (state = defaultState, action) => {
switch (action.type) {
case 'ADD':
return state + action.payload;
default:
return state;
}
};
// 注意这里!!!我们在真正使用使用redux时是不需要的
// store.dispatch方法会触发 Reducer 的自动执行。
const state = reducer(1, {
type: 'ADD',
payload: 2
});
// 我们定义好 reducer之后就可以把这个函数作为参数给 createStore 了
import { createStore } from 'redux';
const store = createStore(reducer);
为啥叫reducer呢?
首先我们js的数组有一个方法叫reduce,reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。
如果我们把什么定义的reducer用上去,看下面的例子是不是懂了!
const actions = [
{ type: 'ADD', payload: 0 },
{ type: 'ADD', payload: 1 },
{ type: 'ADD', payload: 2 }
];
const total = actions.reduce(reducer, 0); // 3
由于 Reducer 是纯函数,就可以保证同样的State,必定得到同样的 View。但也正因为这一点,Reducer 函数里面不能改变 State,必须返回一个全新的对象
reducer的拆分
在学拆分之前,先思考一个点:前面我们说了整个应用只有一个store对象,大型应用有很庞大的数据结构,就会导致有一堆的action,你全部内容都堆在reducer函数里面,你要判断action的type是啥,然后根据state和action中的参数去做出对应的处理,每一个处理必定没有那么简单,都写在reducer函数里,你要是死啊!
这就是reducer拆分的重要性
// 三种 Action 分别改变 State 的三个属性
const chatReducer = (state = defaultState, action = {}) => {
const { type, payload } = action;
switch (type) {
case ADD_CHAT:
return Object.assign({}, state, {
chatLog: state.chatLog.concat(payload)
});
case CHANGE_STATUS:
return Object.assign({}, state, {
statusMessage: payload
});
case CHANGE_USERNAME:
return Object.assign({}, state, {
userName: payload
});
default: return state;
}
};
// 但是他们并没有任何的相关联呀,那我们就把他们拆了
const chatReducer = (state = defaultState, action = {}) => {
return {
chatLog: chatLog(state.chatLog, action),
statusMessage: statusMessage(state.statusMessage, action),
userName: userName(state.userName, action)
}
};
上面👆这个例子虽然已经拆了,但是写法还不够简,能不能再简一点呢?
能!!!
Redux 提供了一个combineReducers
方法,用于 Reducer 的拆分。你只要定义各个子 Reducer 函数,然后用这个方法,将它们合成一个大的 Reducer。
import { combineReducers } from 'redux';
const chatReducer = combineReducers({
chatLog,
statusMessage,
userName
})
// 上面的chatReducer就相当于
const chatReducer = function reducer(state = {}, action) {
return {
chatLog: chatLog(state.chatLog, action),
statusMessage: statusMessage(state.statusMessage, action),
userName: userName(state.userName, action)
}
}
store.subscribe
我们在学这个方法前,先思考一个问题:我们的state发生变化,我们的react怎么马上去更新视图呢?
要知道react中更新视图的方式是react中的props或state发生变更,以class组件为例,要想视图变更,我们可以使用this.setState()方法去触发视图变更,由此我们有了一个方案,我们可以在组件初始化加载完成后加入对redux中state发生变化的监听组件,在state发生变更后,强制进行视图渲染,达到根据state的变进行重新渲染;
于是subscribe
方法就来了;
Store 允许使用store.subscribe
方法设置监听函数,一旦 State 发生变化,就自动执行这个函数。
import { createStore } from 'redux';
const store = createStore(reducer);
// 设置监听
const unsubscribe = store.subscribe(() => {
console.log(store.geState());
});
unsubscribe(); // 解除监听
如上面例子,store.subscribe
方法返回一个函数,调用这个函数就可以解除监听。
小结
通过上面讲的redux基础我们知道了在一个Store的整个实现过程
定义一个reducer
函数作为参数传给createStore
方法,就形成了一个Store;
再由Store提供getState
dispathc
subscribe
方法给ui组件,让其去获取state、修改当前的state、再通过监听state的变化进行一些更新视图的操作;
这就是整个redux的基本实现
四、中间件
在此学习此小节之前,我们先思考一个问题:
我们上面的操作都是Action发出,Reducer立刻去得到最新的State,这种我们叫同步,但是有时候我们想要发出Action以后,过一段时间再执行Reducer,这个就是异步了,但是我们怎么才能在异步操作结束后自动去执行更新state呢?这就用到了我们本节要讲的中间件(middleware);
什么是中间件?
写过node的同学大都接触过middleware的概念,比如express的后端框架都有middleware。那么到什么是一个middleware呢?从字面意思上理解,middleware是“中间件”的意思。我的理解是,一个框架的middleware,就是指这个框架允许我们在某个流程的执行中间插入我们自定义的一段代码。官网的说法是:
It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.
如果你对中间件不熟的话,建议可以去学习一下中间件方面的相关知识;
redux中的middleware也有几个特性:
- Redux的middleware是在 dispatch发起action -> action到达reducer的中间调用的;
- middleware允许链式调用。意思是我们可以注册多个middleware,框架会按照顺序依次调用我们的middleware;
- 如果我们注册了middleware,那么我们在之后每次dispatch的时候都会把所有middleware都走一遍;
具体如何在redux中使用
import { applyMiddleware, createStore } from 'redux';
import createLogger from 'redux-logger';
const logger = createLogger();
const store = createStore(
reducer,
applyMiddleware(logger)
);
如上面👆的例子,redux-logger提供一个生成器createLogger,可以生成日志中间件logger
。然后,将它放在applyMiddleware
方法之中,传入createStore方法,这样我们就在dispatch中加入了一个中间件,进行dispatch相关操作的日志打印;
applyMiddleware
方法有时候是createStore
方法的第二个参数,有时候是第三个参数,当createStore
方法接受整个应用的初始状态(它是第二个参数)作为参数,那样的话,applyMiddleware
方法就是第三个参数了;
const store = createStore(
reducer,
applyMiddleware(thunk, promise, logger)
);
上面代码中,applyMiddleware
方法的三个参数,就是三个中间件。有的中间件有次序要求(从右到左执行),使用前要查一下文档。比如,logger就一定要放在最后,否则输出结果会不正确。
原理
applyMiddleware
是怎么做到往dispatch中传入中间件的呢?
export default function createStore(reducer, enhancer) {
if (enhancer) {
return enhancer(createStore)(reducer);
} else { // return created store
}
}
// compose实现了middleware中的中间件一个个从右到左按顺序执行
import { compose } from 'redux'
export default function applyMiddleware(...middlewares) {
// applyMiddleware返回另一个函数,也就是`enhancer`。
// `enhancer`接受原来的createStore函数为参数.
return function enhancer(createStore) {
// enhancer的返回值是另一个函数,其实就是`新的createStore`
return function enhancedCreateStore(...args) {
// 调用老的createStore,来获得store。
const store = createStore(...args)
// 定义新的dispatch函数,后边会被修改
let dispatch = () => {
throw new Error('Dispatching while constructing your middleware is not allowed.Other middleware would not be applied to this dispatch.')
}
// 暴露个每个middleware的API。
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
// 把所有传入的middlewares转为一个数组。
const chain = middlewares.map(function(middleware) {
return middleware(middlewareAPI)
})
// 新的dispatch函数,其实就是把所有middleware的调用链加入dispatch的执行过程中。
dispatch = compose(...chain)(store.dispatch)
// 新的createStore的返回值,其实唯一变化的就是dispatch字段。
return {
...store,
dispatch,
}
}
}
}
五、异步操作
认识上一节讲的中间件之后我们就可以通过中间件进行 Redux 的一些异步操作
异步操作需要做什么
同步操作只要发出一种 Action 即可,异步操作的差别是它要发出两个 Action(一共三种action):
- 操作发起时的 Action
- 操作成功时的 Action 或 操作失败时的 Action
以向服务器取出数据为例,三种 Action 可以有两种不同的写法:
// 写法一:名称相同,参数不同
{ type: 'FETCH_POSTS' }
{ type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
{ type: 'FETCH_POSTS', status: 'success', response: { ... } }
// 写法二:名称不同
{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }
通过中间件完成异步操作
异步操作至少要送出两个 Action:用户触发第一个 Action,这个跟同步操作一样,没有问题;如何才能在操作结束时,系统自动送出第二个 Action 呢?
有了中间件我们就可以完成异常操作的发送两个Action;
redux-thunk 中间件
完成上面自动发送第二个action的核心就是Action Creator,在Action Creator函数中发起第一个dispatch,然后return一个Promise 对象,在 Promise 中 response 后去再进行第二个dispatch;
const fetchPosts = postTitle => (dispatch, getState) => {
dispatch(requestPosts(postTitle));
return fetch(`/some/API/${postTitle}.json`)
.then(response => response.json())
.then(json => dispatch(receivePosts(postTitle, json)));
};
};
// 使用方法一
store.dispatch(fetchPosts('reactjs'));
// 使用方法二
store.dispatch(fetchPosts('reactjs')).then(() =>
console.log(store.getState())
);
可是我们之前也说了store.dispatch方法正常情况下,参数只能是对象,不能是函数。这时就用到我们的redux-thunk
;
使用redux-thunk中间件,改造store.dispatch,使得后者可以接受函数作为参数。
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducers';
// Note: this API requires redux@>=3.1.0
const store = createStore(
reducer,
applyMiddleware(thunk)
);
因此,异步操作的第一种解决方案就是,写出一个返回函数的 Action Creator,然后使用redux-thunk中间件改造store.dispatch。
redux-promise 中间件
既然 Action Creator 可以返回函数,当然也可以返回其他值。另一种异步操作的解决方案,就是让 Action Creator 返回一个 Promise 对象。
这就需要使用redux-promise中间件。
import { createStore, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise';
import reducer from './reducers';
const store = createStore(
reducer,
applyMiddleware(promiseMiddleware)
);
这个中间件使得store.dispatch方法可以接受 Promise 对象作为参数。这时,Action Creator 有两种写法。
写法一,返回值是一个 Promise 对象。
const fetchPosts = (dispatch, postTitle) => new Promise(function (resolve, reject) {
dispatch(requestPosts(postTitle));
return fetch(`/some/API/${postTitle}.json`)
.then(response => {
type: 'FETCH_POSTS',
payload: response.json()
});
});
写法二,Action 对象的payload属性是一个 Promise 对象。这需要从redux-actions模块引入createAction方法,并且写法也要变成下面这样。
import { createAction } from 'redux-actions';
dispatch(createAction(
'FETCH_POSTS',
fetch(`/some/API/${postTitle}.json`)
.then(response => response.json())
));
注意,createAction的第二个参数必须是一个 Promise 对象。
六、React-Redux
一般我们在react项目中不是直接去使用redux,为了方便使用,我们在react项目经常用到的是React-Redux
React-Redux 设计模式
通过React-Redux,我们把组件拆成两类:UI组件(presentational component)、容器组件(container component);
UI组件特征:
- 只负责 UI 的呈现,不带有任何业务逻辑
- 没有状态(即不使用this.state这个变量)
- 所有数据都由参数(this.props)提供
- 不使用任何 Redux 的 API
容器组件特征:
- 负责管理数据和业务逻辑,不负责 UI 的呈现
- 带有内部状态
- 使用 Redux 的 API
UI组件 和 容器组件 通过React-Redux 提供connect方法从 UI 组件生成容器组件,,React-Redux 规定,所有的 UI 组件都由用户提供,容器组件则是由 React-Redux 自动生成。也就是说,用户负责视觉层,状态管理则是全部交给它。
connect()
我们上面说了connect
方法,用于从 UI 组件生成容器组件,connect
的意思,就是将这两种组件连起来。
import { connect } from 'react-redux'
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
其中看到的 TodoList 就是UI组件,VisibleTodoList 就是我们使用connect
方法生成的容器组件;
connect
方法接受两个参数:mapStateToProps
和mapDispatchToProps
。它们定义了 UI 组件的业务逻辑。前者负责输入逻辑,即将state映射到 UI 组件的参数(props),后者负责输出逻辑,即将用户对 UI 组件的操作映射成 Action;
输入逻辑:外部的数据(即state对象)如何转换为 UI 组件的参数
输出逻辑:用户发出的动作如何变为 Action 对象,从 UI 组件传出去。
mapStateToProps
mapStateToProps
是connect
方法的第一个参数,它是一个函数。它的作用就是像它的名字那样,建立一个从(外部的)state对象到(UI 组件的)props对象的映射关系;
作为函数,它可以接收两个参数state和ownProps,state对象来着store,而ownProps是容器组件的props对象;
mapStateToProps
执行后应该返回一个对象,里面的每一个键值对就是一个映射,这些键值就是UI组件的props;
const mapStateToProps = (state, ownProps) => {
return {
active: ownProps.filter === state.visibilityFilter
}
}
如上面的例子,U组件的props就会有一个active的属性,每次store中的state 或者 容器组件的props发生变更是active就会发生变更,UI组件重新渲染;
connect方法可以省略mapStateToProps
参数,那样的话,UI 组件就不会订阅Store,就是说 Store 的更新不会引起 UI 组件的更新;
mapDispatchToProps
mapDispatchToProps
是connect
方法的第二个参数,用来建立 UI 组件的参数到store.dispatch方法的映射。也就是说,它定义了哪些用户的操作应该当作 Action,传给 Store;它可以是一个函数,也可以是一个对象。
当mapDispatchToProps
是函数时:
它可以接收两个参数dispatch和ownProps,dispatch来自store,而ownProps是容器组件的props对象;
const mapDispatchToProps = (dispatch, ownProps) => {
return {
onClickBtn: () => {
dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: ownProps.filter
});
}
};
}
如上面的例子,U组件的props就会有一个onClickBtn的属性,在UI组件中使用this.props.onClickBtn()
方法就可以向store发送一个Action,以此来进行store中state的更新;
当mapDispatchToProps
是对象时:
它的每个键名也是对应 UI 组件的同名参数,键值应该是一个函数,会被当作 Action creator ,返回的 Action 会由 Redux 自动发出。
const mapDispatchToProps = {
onInputChange: (value) => {
type: 'SET_INPUT_VALUE',
payload: value,
};
}
如上面的例子,U组件的props就会有一个onInputChange的属性,在UI组件中使用this.props.onInputChange()
方法就可以向store发送一个Action,以此来进行store中state的更新;
<Provider> 组件
在开始这个组件学习前,我们先思考一个问题:无论是mapStateToProps
还是mapDispatchToProps
他们中的第一个参数state、dispatch都是从store中订阅的,那就有一个问题了,怎么订阅来的呢?或者说state、dispatch是怎么传到mapStateToProps
或mapDispatchToProps
中的呢?
这就需要用到<provider>
组件,Provider的作⽤是从最外部封装了整个应⽤,并向connect模块传递store,在该组件里边的所有组件都可以使用 store,使 React 组件可被连接(connectable)
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import reducers from './reducers';
import App from './components/App'
let store = createStore(todoApp);
const App = (
<Provider store={store}>
<App />
</Provider>
)
class Counter extends Component {
componentDidMount() {
const { store } = this.context;
this.unsubscribe = store.subscribe(() =>
this.forceUpdate()
);
}
render() {
const props = this.props;
const { store } = this.context;
const state = store.getState();
// ...
}
}
Counter.contextTypes = {
store: React.PropTypes.object
}
const App = connect(
mapStateToProps,
mapDispatchToProps
)(Counter)
export default App
看上面的例子,我们把App组件写在Provider组件内后:
- 所有其下面的所有组件都能通过this.context去拿到
store
- mapStateToProps、mapDispatchToProps都能通过connect与store建立联系,订阅到state和dispatch
React-Redux实例
接下来我们通过React-Redux来写一个计数器组件的实例:
1、首先写一个reducer函数作为createStore
方法的参数
// reducer.js
const initState = {
count: 0,
}
const computerReducer = (state = initState, action) => {
const count = state.count
switch (action.type) {
case 'increase':
return { count: count + 1 }
default:
return state
}
}
export default computerReducer;
2、创建store对象,并使用Provider在根组件外面包一层。
// index.js
import ReactDOM from 'react-dom'
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import computerReducer from './reducer.js';
import App from './App.tsx'; //这个容器组件我们在后面写
const store = createStore(computerReducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
3、完成上面的操作之后App内的所有子组件都能去使用store,接下来示例App组件用上store
3.1创建一个UI组件
// Counter.jsx
import { Component } from 'react'
class Counter extends Component {
render() {
const { value, onIncreaseClick } = this.props
return (
<div>
<span>{value}</span>
<button onClick={onIncreaseClick}>Increase</button>
</div>
)
}
}
export default Counter;
3.2通过connect生成容器组件(App)
// App.jsx
import { connect } from 'react-redux';
import Counter from './Counter.tsx';
const mapStateToProps = (state) => {
return {
value: state.count
}
}
// Action Creator
const increaseAction = { type: 'increase' }
const mapDispatchToProps = (dispatch) => {
return {
onIncreaseClick: () => dispatch(increaseAction)
}
}
const App = connect(
mapStateToProps,
mapDispatchToProps
)(Counter);
export default App;
以上就是一个React-Redux的简单示例
总结
项目中一个的任务状态管理远没有上面示例这么简单,就需要我们结合reducer的拆分
、中间件的结合
等去组合设计,是不是很繁琐,耐心去想想自己的项目是不是真的需要用到redux,或者是否有更好的任务状态管理器可以去代替它;
我在学习react-redux参考的是 阮一峰大佬的网络日志,但是它写的redux文档年份很久是2016年的,所以到现在已经有很多redux的新玩法了,比如上面是通过类组件的方式去写的,那么函数组件的方式怎么写呢?我们可以是不是可以去写一写看看,不管怎么变它的基本实现思路就是上面API,只要掌握上面这些,换个玩法照样很快就能写出来
参考
Redux 入门教程(二):中间件与异步操作 - 阮一峰的网络日志
Redux 入门教程(三):React-Redux 的用法 - 阮一峰的网络日志