上一篇分析了Flux出现的背景和原理,最核心的思想就是“组件化+单向数据流”。javascript
可是,Flux在设计上并不是完美,具体来讲主要存在如下2个不足:前端
因为Flux采用多Store设计,各个Store之间可能存在数据依赖。以flux-chat为例:在这个聊天软件里,可能会有多我的给你发消息,好比Dave给你发了3条,Brian给你发了2条,当你点开某我的给你发的消息后,界面须要刷新,显示你目前还有几我的的未读消息没有查看:java
为了解决这个需求,建立了3个Store:react
当你点开某个消息组时,显然你须要先更新ThreadStore和MessageStore,而后再更新UnreadThreadStore。因为Store的注册顺序是不肯定的,为了应付这种依赖,Flux提供了waitFor()机制,每一个Store在注册以后都会生成一个令牌(dispatchToken),经过等待令牌的方式确保其余Store被优先更新。数据库
所以UnreadThreadStore的代码会写成下面这个样子:编程
Dispatcher.waitFor([
ThreadStore.dispatchToken,
MessageStore.dispatchToken
]);
switch (action.type) {
case ActionTypes.CLICK_THREAD:
UnreadThreadStore.emitChange();
break;
...
}
复制代码
虽然能够工做,可是总以为不是很优雅,在一个Store中须要显示地包含其余Store的调用。固然你会说,干脆把这3个Store的代码糅到一块儿,搞成一个Store不就好了?可是这样又会致使代码结构不够清晰,不利于多模块分工协做。redux
为了兼顾这两个方面,Redux使用全局惟一Store,外部可使用多个reducer来修改Store的不一样部分,最后会把全部reducer的修改再组合成一个新的Store状态。bash
所谓纯函数,是指输出只和输入相关,相同的输入必定会获得相同的输出。用专业一点的术语来讲,纯函数没有“反作用”。咱们先来看看Flux中是怎么修改状态的:网络
Dispatcher.register(action => {
switch(action.type) {
case ActionTypes.CLICK_THREAD:
_currentID = action.threadID;
ThreadStore.emitChange();
break;
...
}
复制代码
能够看到,是直接修改变量值,而后显式发送一个change事件来通知View。app
咱们再来看看Redux中是怎么修改状态的:
export default function threadReducer(state = {}, action) {
switch (action.type) {
case ActionTypes.CLICK_THREAD: {
return { ...state, _currentID: action.threadID };
...
}
复制代码
细心的人可能已经看出来了,主要有3点区别:
那么有人会说了,为啥要这么作,好像也没看到啥好处嘛?固然是有好处的,这样能够支持“时间旅行调试(Time Travel Debugging)”。所谓时间旅行调试,指的是能够支持状态的无限undo / redo。因为state对象是被总体替换的,若是想回到上一个状态从新执行,那么直接替换成上一步的state对象就能够了。
首先咱们要搞清楚,Redux解决了哪些问题?主要是如下3点:
1.如何在应用程序的整个生命周期内维持全部数据?
Redux是一个“状态容器”。写过React或者ReactNative的同窗可能会有感觉,若是多个页面须要共享数据时,须要把数据一层层地传递下去,很是繁琐。若是能有一个全局统一的地方存储数据,当数据发生变化时自动通知View刷新界面,是否是很美好呢?所以,咱们须要一个“状态容器”。
2.如何修改这些数据?
Redux借鉴了分布式计算中的map-reduce的思想,把Store中的数据分割(map)成多个小的对象,经过纯函数修改这些对象,最后再把全部的修改合并(reduce)成一个大的对象。修改数据的纯函数被称为reducer。
3.如何把数据变动传播到整个应用程序?
经过订阅(subscribe)。若是你的View须要跟随数据的变化动态刷新,能够调用subscribe()注册回调函数。在这一点上,Redux是很是粗粒度的,每次只要有新的action被分发,你都会收到通知。显然,你须要对通知进行过滤,这意味着你可能会写不少重复代码。不过,这也是出于通用性和灵活性考虑,实际上Redux不只能够用于React,也能够用在Vue.js或者Angular上。能够搭配特定框架相关的适配层好比react-redux来规避这些重复代码。
说了这么多,咱们来看一下Redux的基本框架:
和前一篇的Flux框架图对比一下能够发现,Redux去除了dispatcher组件(由于只有一个Store),增长了recuder组件(用于更新Store的不一样部分)。下面详细介绍各个部分的做用。
首先咱们须要建立一个全局惟一的Store,Redux提供了辅助函数createStore():
import { createStore } from 'redux'
var store = createStore(() => {})
复制代码
你可能注意到了,createStore()须要提供一个参数,这个参数就是reducer。
前面介绍过,reducer就是一个纯函数,输入参数是state和action,输出新的state。通常的代码模板以下:
var reducer = (state = {}, action) => {
switch (action.type) {
case 'MY_ACTION':
return {...state, message: action.message}
default:
return state
}
}
复制代码
须要注意的是,default分支必定要返回state,不然会致使状态丢失。
好了,如今咱们有了reducer,能够做为参数传递给4.1节中的createStore()函数了。
createStore()只能接受一个reducer参数,若是咱们有多个reducer怎么办?这时须要使用另外一个辅助函数combineReducers():
import { combineReducers } from 'redux'
var reducer = combineReducers({
first: firstReducer,
second: secondReducer
})
复制代码
combineReducers()会把多个reducer组合成一个,当有action过来时会依次调用每一个子reducer,因此实际上你能够组织成一个树状结构。
所谓action,其实就是一个普通的javascript对象,通常会包含一个type属性用于标识类型,以及一个payload属性用于传递参数(名字能够随便取):
var action = {
type: 'MY_ACTION',
payload: { message: 'hello' }
}
复制代码
那么如何发送action呢?store提供了一个dispatch()函数:
store.dispatch(action)
复制代码
所谓action creator,其实就是一个用来构建action对象的函数:
var actionCreator = (message) => {
return {
type: 'MY_ACTION',
payload: { message: message }
}
}
复制代码
因此4.3节发送action的代码也能够写成这样:
store.dispatch(actionCreator('hello'))
复制代码
当你发送了一个action,reducer被调用并完成状态修改,那么前端视是怎么感知到状态变化的呢?咱们须要经过subscribe()进行订阅:
store.subscribe(() => {
let state = store.getState()
... ...
})
复制代码
store的getState()函数能够得到当前状态的一个副本,而后就能够刷新界面了,以React为例,能够调用this.setState()或者this.forceUpdate()触发从新渲染。
当视图组件比较多时,每次都要写这段订阅代码会比较繁琐,后面会介绍经过react-redux来简化这一过程。
第3章的那张图其实还少画了个东西,叫作middleware(中间件)。那么这个middleware是干什么用的呢?
在Web应用中常常会有异步调用,好比请求网络、查询数据库什么的。咱们首先发送一个action启动异步任务,并但愿在异步任务完成之后再更新状态,应该如何实现呢?在Flux中,咱们能够在dispatcher里完成:首先启动异步任务,而后在回调函数中再发送一个新的action去更新Store。可是Redux中去除了dispatcher的概念,你能调用的只有store的dispatch()函数而已,那咱们该怎么办呢?答案就是middleware。
因此,Redux的完整流程应参见下面这张动图:
咱们先来看一个简单的middleware的例子:
var thunkMiddleware = ({ dispatch, getState }) => {
return (next) => {
return (action) => {
return typeof action === 'function' ?
action(dispatch, getState) :
next(action)
}
}
}
复制代码
能够发现,其实middleware就是一个三层嵌套的函数:
因此,实际上middleware能够理解在action进入reducer以前进行了一次拦截。在这个例子里,若是action是一个函数,咱们就不会把action继续传递下去,而是调用这个函数去执行异步任务。当异步任务执行完毕后,咱们能够调用dispatch()函数发送一个新的action,用于调用reducer更新状态。
那么咱们如何注册一个中间件呢?Redux提供了一个工具函数applyMiddleware(),能够直接做为createStore()的一个参数传递进去:
const store = createStore(
reducer,
applyMiddleware(myMiddleware1, myMiddleware2)
)
复制代码
预告一下,后面一篇要介绍的redux-saga,其实就是一个Redux中间件。
Redux的设计主要考虑的是通用性和灵活性,若是想更好的配合React的组件化编程习惯,你可能须要react-redux。
Redux使用全局惟一的Store,另外当你须要发送action的时候,必须经过store的dispatch()函数。这对于一个有不少页面的React应用来讲,意味着只有两种选择:
这显然极其繁琐,幸运的是,React提供了Context机制,说白了就是全部页面都能访问的一个上下文对象:
react-redux利用React的Context机制进行了封装,提供了<Provider>组件和connect()函数来实现store对象的全局可访问性。
这是一个React组件,使用时须要把它包裹在应用层根组件的外面,而后把全局store对象赋值给它的store属性:
import { Provider } from 'react-redux'
import store from './mystore'
export default class Application extends React.Component {
render () {
return (
<Provider store={ store }>
<Home />
</Provider>
)
}
}
复制代码
Provider组件只是把store对象放进了Context中,若是你须要访问它,还须要一些额外的代码,react-redux提供了一个connect()函数来帮你完成这些工做。
实际上,connect()就帮你作了两件事:
实现层面上,connect()采用了React的HOC(高阶组件)技术,动态建立新组件及其实例:
那么这个connect()怎么用呢?咱们经过3个应用场景依次介绍。
1.你只是但愿能在组件中使用dispatch()直接派发action
这是最简单的状况,你只须要在导出组件的时候加上connect()就能够了:
export default connect()(MyComponent)
复制代码
当你须要派发action的时候,能够直接调用this.props.dispatch()。
2.你不想直接使用dispatch(),但愿可以自动派发action
实际上你会发现,若是action不少的话,你须要不停地调用dispatch()函数。为了使咱们的实现更加“声明式”,最好是把派发逻辑封装起来。实际上Redux中有一个辅助函数bindActionCreators()来完成这项工做,它会为每一个action creator生成同名的函数,自动调用dispatch()函数:
const increment = () => ({ type: "INCREMENT" });
const decrement = () => ({ type: "DECREMENT" });
const boundActionCreators = bindActionCreators({ increment, decrement }, dispatch);
// 返回值:
// {
// increment: (...args) => dispatch(increment(...args)),
// decrement: (...args) => dispatch(decrement(...args)),
// }
复制代码
这样你就能够直接调用boundActionCreators.increment()派发action了。那么如何跟connect()联系起来呢?这里须要用到它的第2个参数(第1个参数后面再介绍)mapDispatchToProps,举个例子:
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({ increment, decrement }, dispatch);
}
export default connect(null, mapDispatchToProps)(MyComponent)
复制代码
这样,你就能够在组件中直接调用this.props.increment()函数了。
你觉得这样就结束了?还有更简单的方法,连bindActionCreators()都不用写!你能够直接提供一个对象,包含全部的action creator就好了(这被称为“对象简写”方式):
const mapDispatchToProps = { increment, decrement }
export default connect(null, mapDispatchToProps)(MyComponent)
复制代码
注意:若是你提供了mapDispatchToProps参数,那么默认状况下dispatch就不会再注入到props中了。若是你还想使用this.props.dispatch(),能够在mapDispatchToProps的返回值对象中加上dispatch属性。
3.你但愿访问store中的数据
这应该是使用最多的场景,组件访问store中的数据并刷新界面。根据“无状态组件”设计原则,咱们不该该直接访问store,而须要经过一个“selector函数”把store中的数据映射的props中进行访问,这个“selector函数”就是conntect()的第1个参数mapStateToProps。举个例子:
const mapStateToProps = (state = {}, ownProps) => {
return {
xxx: state.xxx
}
}
export default connect(mapStateToProps)(MyComponent)
复制代码
这样你在组件中就能够经过this.props.xxx进行访问了。另外,它还会帮你自动订阅store,任什么时候候store状态数据发生变化,mapStateToProps都会被调用并致使界面从新渲染。除了第一个参数state以外,还有一个可选参数ownProps,若是你的组件须要用自身的props数据到store中检索数据,能够经过这个参数获取。
固然,你能够同时提供mapStateToProps和mapDispatchToProps参数,这样你就能够得到两方面的功能:
export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)
复制代码
最后,以一张思惟导图结束本篇文章,下一篇介绍redux-saga。