提及Flux,笔者以前,曾写过一篇《ReFlux细说》的文章,重点对比讲述了Flux的另外两种实现形式:『Facebook Flux vs Reflux』,有兴趣的同窗能够一并看看。node
时过境迁,如今社区里,Redux的风头早已盖过其余Flux,它与React的组合使用更是你们所推荐的。react
Redux很火,很流行,并非没有道理!!它自己灵感来源于Flux,但却不局限于Flux,它还带来了一些新的概念和思想,集成了immutability的同时,也促成了Redux自身生态圈。git
笔者在看完redux和react-redux源码后,以为它的一些思想和原理拿出来聊一聊,会更有利于使用者的了解和使用Redux。github
(注
:若是你是初学者,能够先阅读一下Redux中文文档,了解Redux基础知识。)express
做为Flux的一种实现形式,Redux天然保持着数据流的单向性
,用一张图来形象说明的话,能够是这样:redux
上面这张图,在展示单向数据流的同时,还为咱们引出了几个熟悉的模块:Store、Actions、Action Creators、以及Views。设计模式
相信你们都不会陌生,由于它们就是Flux设计模式中所提到的几个重要概念,在这里,Redux沿用了它们,并在这基础之上,又融入了两个重要的新概念:Reducers
和Middlewares
(稍后会讲到)。api
接下来,咱们先说说Redux在已有概念上的一些变化,以后再聊聊Redux带来的几个新概念。数组
Store — 数据存储中心,同时链接
着Actions和Views(React Components)。
链接
的意思大概就是:
setState
进行从新渲染组件(re-render)。上面这三步,实际上是Flux单向数据流所表达出来的思想,然而要实现这三步,才是Redux真正要作的工做。
下面,咱们经过答疑的方式,来看看Redux是如何实现以上三步的?
问:Store如何接收来自Views的Action?
答:每个Store实例都拥有dispatch
方法,Views只须要经过调用该方法,并传入action对象做为形参,Store天然就就能够收到Action,就像这样:
store.dispatch({ type: 'INCREASE' });
问:Store在接收到Action以后,须要根据Action.type和Action.payload修改存储数据,那么,这部分逻辑写在哪里,且怎么将这部分逻辑传递给Store知道呢?
答:数据修改逻辑写在Reducer(一个纯函数)里,Store实例在建立的时候,就会被传递这样一个reducer做为形参,这样Store就能够经过Reducer的返回值更新内部数据了,先看一个简单的例子(具体的关于reducer咱们后面再讲):
// 一个reducer function counterReducer(state = 0, action) { switch (action.type) { case 'INCREASE': return state + 1; case 'DECREASE': return state - 1; default: return state; } } // 传递reducer做为形参 let store = Redux.createStore(counterReducer);
问:Store经过Reducer修改好了内部数据以后,又是如何通知Views须要获取最新的Store数据来更新的呢?
答:每个Store实例都提供一个subscribe
方法,Views只须要调用该方法注册一个回调(内含setState操做),以后在每次dispatch(action)
时,该回调都会被触发,从而实现从新渲染;对于最新的Store数据,能够经过Store实例提供的另外一个方法getState
来获取,就像下面这样:
let unsubscribe = store.subscribe(() => console.log(store.getState()) );
因此,按照上面的一问一答,Redux.createStore()
方法的内部实现大概就是下面这样,返回一个包含上述几个方法的对象:
function createStore(reducer, initialState, enhancer) { var currentReducer = reducer var currentState = initialState var listeners = [] // 省略若干代码 //... // 经过reducer初始化数据 dispatch({ type: ActionTypes.INIT }) return { dispatch, subscribe, getState, replaceReducer } }
总结概括几点:
dispatch(action)
来完成,即:action -> reducers -> store消息发布/订阅
(pub/sub)的功能,也正是由于这个功能,它才可以同时链接
着Actions和Views。
Reducer,这个名字来源于数组的一个函数 — reduce,它们俩比较类似的地方在于:接收一个旧的prevState,返回一个新的nextState。
在上文讲解Store的时候,得知:Reducer是一个纯函数,用来修改Store数据的。
这种修改数据的方式,区别于其余Flux,因此咱们疑惑:经过Reducer修改数据给咱们带来了哪些好处?
这里,我列出了两点:
Redux有一个原则:单一数据源
,即:整个React Web应用,只有一个Store,存储着全部的数据。
这个原则,其实也不难理解,假若多个Store存在,且Store之间存在数据关联的状况,处理起来每每会是一件比较头疼的事情。
然而,单一Store存储数据,就有可能面临着另外一个问题:数据结构嵌套太深,数据访问变得繁琐,就像下面这样:
let store = { a: 1, b: { c: true, d: { e: [2, 3] } } }; // 增长一项: 4 store.b.d.e = [...store.b.d.e, 4]; // es7 spread console.log(store.b.d.e); // [2, 3, 4]
这样的store.b.d.e
数据访问和修改方式,对于刚接手的项目,或者不清楚数据结构的同窗,简直是晴天霹雳!!
为此,Redux提出经过定义多个reducer对数据进行拆解
访问或者修改,最终再经过combineReducers
函数将零散的数据拼装
回去,将是一个不错的选择!
在JavaScript中,数据源其实就是一个object tree,object中的每个key均可以认为是tree的一个节点,每个叶子节点都含有一个value(非plain object),就像下面这张图所描述的:
而咱们对数据的修改,其实就是对叶子节点value的修改,为了不每次都从tree的根节点r开始访问,能够为每个叶子节点建立一个reducer,并将该叶子节点的value直接传递给该reducer,就像下面这样:
// state 就是store.b.d.e的值 // [2, 3]为默认初始值 function eReducer(state = [2, 3], action) { switch (action.type) { case 'ADD': return [...state, 4]; // 修改store.b.d.e的值 default: return state; } }
如此,每个reducer都将直接对应数据源(store)的某一个字段(如:store.b.d.e),这样的直接的修改方式会变得简单不少。
拆解以后,数据就会变得零散,要想将修改后的数据再从新拼装
起来,并统一返回给store,首先要作的就是:将一个个reducer自上而下一级一级地合并起,最终获得一个rootReducer。
合并reducer时,须要用到Redux另外一个api:combineReducers
,下面这段代码,是对上述store的数据拆解:
import { combineReducers } from 'redux'; // 叶子reducer function aReducer(state = 1, action) {/*...*/} function cReducer(state = true, action) {/*...*/} function eReducer(state = [2, 3], action) {/*...*/} const dReducer = combineReducers({ e: eReducer }); const bReducer = combineReducers({ c: cReducer, d: dReducer }); // 根reducer const rootReducer = combineReducers({ a: aReducer, b: bReducer });
这样的话,rootReducer的返回值就是整个object tree。
总结一点:Redux经过一个个reducer完成了对整个数据源(object tree)的拆解访问和修改。
React在利用组件(Component)构建Web应用时,其实无形中建立了两棵树:虚拟dom树
和组件树
,就像下图所描述的那样(原图):
因此,针对这样的树状结构,若是有数据更新,使得某些组件应该获得从新渲染(re-render)的话,比较推荐的方式就是:自上而下渲染
(top-down rendering),即顶层组件经过props传递新数据给子孙组件。
然而,每次须要更新的组件,可能就是那么几个,可是React并不知道,它依然会遍历执行每一个组件的render方法,将返回的newVirtualDom和以前的prevVirtualDom进行diff比较,而后最后发现,计算结果极可能是:该组件所产生的真实dom无需改变!/(ㄒoㄒ)/~~(无用功致使的浪费性能)
因此,为了不这样的性能浪费,每每咱们都会利用组件的生命周期函数shouldComponentUpdate
进行判断是否有必要进行对该组件进行更新(即,是否执行该组件render方法以及进行diff计算)?
就像这样:
shouldComponentUpdate(nextProps) { if (nextProps.e !== this.props.e) { // 这里的e是一个字段,多是对象引用,也多是数值,布尔值 return true; // 须要更新 } return false; // 无需更新 }
但,每每这样的比较,对于字面值还行,对于对象引用(object,array),就糟糕了,由于:
let prevProps = { e: [2, 3] }; let nextProps = prevProps; nextProps.e.push(4); console.log(prevProps.e === nextProps.e); // 始终为true
虽然你能够经过deepEqual来解决这个问题,但对嵌套较深的结构,性能始终会是一个问题。
因此,最后对于对象引用的比较,就引出了不可变数据
(immutable data)这个概念,大致的意思就是:一个数据被建立了,就不能够被改变(mutation)。
若是你想改变数据,就得从新建立一个新的数据(即新的引用),就像这样:
let prevProps = { e: [2, 3] }; let nextProps = { e:[...prevProps.e, 4] // es7 spread }; console.log(prevProps.e === nextProps.e); // false
也许,你已经发现每一个Reducer函数在修改数据的时候,正是这样作的,最后返回的都是一个新的引用,而不是直接修改引用的数据,就像这样:
function eReducer(state = [2, 3], action) { switch (action.type) { case 'ADD': return [...state, 4]; // 并无直接地经过state.push(4),修改引用的数据 default: return state; } }
最后,由于combineReducers
的存在,以前的那个object tree的总体数据结构就会发生变化,就像下面这样:
如今,你就能够在shouldComponentUpdate
函数中,肆无忌惮地比较对象引用了,由于数据若是变化了,比较的就会是两个不一样的对象!
总结一点:Redux经过一个个reducer实现了不可变数据
(immutability)。
PS:固然,你也能够经过使用第三方插件(库)来实现immutable data,好比:React.addons.update、Immutable.js。(只不过在Redux中会显得那么没有必要)。
Middleware — 中间件,最初的思想毫无疑问来自:Express。
中间件讲究的是对数据的流式处理
,比较优秀的特性是:链式组合
,因为每个中间件均可以是独立的,所以能够造成一个小的生态圈。
在Redux中,Middlerwares要处理的对象则是:Action
。
每一个中间件能够针对Action的特征,能够采起不一样的操做,既能够选择传递给下一个中间件,如:next(action)
,也能够选择跳过某些中间件,如:dispatch(action)
,或者更直接了当的结束传递,如:return
。
标准的action应该是一个plain object,可是对于中间件而言,action还能够是函数,也能够是promise对象,或者一个带有特殊含义字段的对象,但无论怎样,由于中间件会对特定类型action作必定的转换,因此最后传给reducer的action必定是标准的plain object。
好比说:
action.meta.delay
,具体以下:// 用 { meta: { delay: N } } 来让 action 延迟 N 毫秒。 const timeoutScheduler = store => next => action => { if (!action.meta || !action.meta.delay) { return next(action) } let timeoutId = setTimeout( () => next(action), action.meta.delay ) return function cancel() { clearTimeout(timeoutId) } }
那么问题来了,这么多的中间件,如何使用呢?
先看一个简单的例子:
import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import createLogger from 'redux-logger'; import rootReducer from '../reducers'; // store扩展 const enhancer = applyMiddleware( thunk, createLogger() ); const store = createStore(rootReducer, initialState, enhancer); // 触发action store.dispatch({ type: 'ADD', num: 4 });
注意:单纯的Redux.createStore(...)
建立的Store实例,在执行store.dispatch(action)
的时候,是不会执行中间件的,只是单纯的action分发。
要想给Store实例附加上执行中间件的能力,就必须改造createStore
函数,最新版的Redux是经过传入store扩展(store enhancer)来解决的,而具备中间件功能的store扩展,则须要使用applyMiddleware
函数生成,就像下面这样:
// store扩展 const enhancer = applyMiddleware( thunk, createLogger() ); const store = createStore(rootReducer, initialState, enhancer);
上面的写法是新版Redux才有的,之前的写法则是这样的(新版兼容的哦):
// 旧写法 const createStoreWithMiddleware = applyMiddleware( thunk, createLogger() )(createStore); const store = createStoreWithMiddleware(reducer, initialState)
至于改造后的createStore
方法为什么拥有了执行中间件的能力,你们能够看一下appapplyMiddleware
的源码。
最后,简单用一张图来验证一句话的正确性:中间件提供的是位于 action 被发起以后,到达 reducer 以前的扩展点。
为了让Redux可以更好地与React配合使用,react-redux库的引入就显得必不可少。
react-redux主要暴露出两个api:
Provider存在的意义在于:想经过context的方式将惟一的数据源store传递给任意想访问的子孙组件。
好比,下面要说的connect方法在建立Container Component时,就须要经过这种方式获得store,这里就不展开说了。
不熟悉React context的同窗,能够看看官方介绍。
Redux中的connect方法,跟Reflux.connect
方法有点相似,最主要的目的就是:让Component与Store进行关联,即Store的数据变化能够及时通知Views从新渲染。
下面这段源码(来自connect.js),可以说明上述观点:
trySubscribe() { if (shouldSubscribe && !this.unsubscribe) { // 跟store关联,消息订阅 this.unsubscribe = this.store.subscribe(this.handleChange.bind(this)) this.handleChange() } } handleChange() { if (!this.unsubscribe) { return } const prevStoreState = this.state.storeState const storeState = this.store.getState() if (!pure || prevStoreState !== storeState) { this.hasStoreStateChanged = true this.setState({ storeState }) // 组件从新渲染 } }
另外,connect方法,还引出了另外两个概念,即:容器组件(Container Component)和展现组件(Presentational Component)。
感兴趣的同窗,能够看下这篇文章《Presentational and Container Components》,了解二者的区别,这里就不展开讨论了。
以上就是笔者对Redux及其相关知识的理解,不对的地方欢迎留言交流,新浪微博。