这个是很久以前写的,一直忘记粘过来,里面有一些是写做格式是我本身定义的,因此和segmentfault的markdown语法有出入,图片也不能加载,因此原文效果能够在原网站上浏览,敬请赐教。javascript
<------------
文章原网址
------------>html
Redux is a predictable state container for JavaScript apps1.vue
在过去的十几年里web page一直都以指数递增的方式发展,不管是概念上仍是编程上想要完全读懂这门设计的艺术已经变得不可为,现在在许多大型网站中的一个页面从DOM
与Event
的微观角度每每汇集着一系列复杂并琐碎的行为功能2,它们聚合在一块儿构成了咱们今天能够在浏览器端可操做的视图,正是如此,怎样管理这些行为功能被提上日程,诸多才华横溢的工程师们引伸出状态管理的概念,制做出许多优秀的做品,如Redux、flux、flummox、mobxjs、refluxjs、martyjs、javascript-state-machine、vuex等,其中又以redux
和flux
最为流行。java
Redux诞生的出发点是做为一个javascript应用状态(state)容器,借鉴flux的数据单向流动、elm的The Elm Architecture、函数式编程、柯里化(Currying)函数、组合模式(Composite pattern)的等思想。将视图(view)中可操做的这些行为类比为动做(action),每一个动做传递都附带状态(state)信息,状态引导动做对redux状态容器store更新进而对视图更新,store对状态(state)进行同一管理,能够说store可预测状态容器是redux的骨架也不为过。node
不管什么框架都会设定一些属于它的规则,规则恒定,附者云起进而造成生态,react如是redux也是如此,在redux中因此规定了三条原则(Three Principles),即“Single source of truth”、“State is read-only”和“Changes are made with pure functions”,用于描述在redux整个生命周期内怎样去管理和维护store树。react
Single source of truth:惟一数据源。State被存储在一颗惟一的object tree
上,即store对象树。git
State is read-only:State只读。在每一个组件(Component)或者reducer等内部,State树内全部key->value
只读。github
Changes are made with pure functions:这里的纯函数(pure functions)特指reducer函数。State树内的state只能依靠纯函数reducer对store进行更新。web
姑且先讲reducer函数怎样更新store放下,先讨论store变动为何会引发state变化。这里就要引伸到单向数据流(Unidirectional data flow)的理论,单向数据流动即从模型到视图的数据流动,它区别于双向数据绑定
的方式,用react中的术语解释的话就是,当某个组件的数据prop
须要变化而且经过相关方法操做更新store对象树内某个碎片state以后,redux会返回一个新的store会从父节点传递到子节点,依次向下遍历整棵组件树,以组件为单位寻找使用了变化的prop
的组件进行渲染。vuex
假定一个react渲染的页面,黄色部分表明页面DOM
结构树,蓝色是各个组件,组件之间的包含关系为A ⊇ {B, C} && C ⊇ D
,其中B
和C
子组件都引用了store树上的props.test
属性,这是一个很是典型的从上到下单向流动的阶梯式模型。
如今发生变化,当在B
组件内某个操做(UI交互、API调用等)更新了state值(即props.test
属性值),这个操做自己并不会对B
组件的视图和渲染进行干扰和操做,可是B
组件和D
组件会在store树内的相应state值变化后触发组件使view发生改变。若是是在传统页面中,这是事件和DOM结构之间的一对一,在数据双向绑定概念中是事件与DOM结构的多对多,在react开发中应该是事件与VDOM一对一,可是在redux接管数据源后就变成了事件与VDOM之间没有直接关系,VDOM的渲染间接由store对象树决定。
紫色线条是数据流向,紫色方框是触发渲染的子组件,数据从顶层store开始向下流淌,store顶层数据发生改变后会分别触发存在props.test
属性的子组件进行从新渲染更新DOM
,以达到视图渲染可控、状态重现可控的目的。
<!--
-->
Store做为状态容器、惟一数据源,由一个createStore(reducer, preloadedState, enhancer)
函数建立,在项目部署过程当中使用createStore函数通常会在项目根目录下单独列一个文件。
<pre class="pre-no-border">
.
├── bin
│ └── ...
├── ...
├── src
│ ├── components
│ │ └── ...
│ ├── containers
│ │ └── ...
│ ├── routes
│ │ └── ...
│ ├── store
│ │ ├── createStore.js
│ │ └── reducers.js
│ └── utils
│ └── ...
└── ...
</pre>
import {createStore} from 'redux'; import makeRootReducer from './reducers'; ... export default (initialState = {}, history) => { const store = createStore( makeRootReducer(), initialState, ); return store }
在__createStore.js__文件中createStore函数传入了__makeRootReducer__和__initialState__两个值,其中makeRootReducer就是一般所说的root reducer,只不过本文所示例的reducer皆为异步加载,因此可能和其它文章写的root reducer方式不同,详细的内容下文会有叙述。阅读redux源码的createStore函数能够看到最后返回四个核心对象和一个symbol对象,也就是说在示例中makeRootReducer()=reducer
、initialState=preloadedState
。
import $$observable from 'symbol'; export default function createStore(reducer, preloadedState, enhancer) { ... return { dispatch, subscribe, getState, replaceReducer, [$$observable]: observable } }
方法 | 使用方式 | 描述 |
---|---|---|
dispatch | store.dispatch(action) | action参数将参与store更新,并分发给subscribe函数正在监听的reducer |
subscribe | store.subscribe(listener) | listener监听者,实际上就是回调函数 |
getState | store.getState() | 获取state |
replaceReducer | store.replaceReducer(nextReducer) | 刷新reducer并初始化store |
Dispatch方法用于更新store状态树,流程是在dispatch接受一个action,由action决定调用reducer转换状态树, 且通知监听者数据已发生变化,从dispatch源码中看到函数currentReducer(currentState, action)
传递state、action,观察者列表listeners
直接for循环遍历执行listeners[i]()
。
function dispatch(action) { ... currentState = currentReducer(currentState, action) ... var listeners = currentListeners = nextListeners for (var i = 0; i < listeners.length; i++) { listeners[i]() } return action }
在主流的redux思想里有一种说法叫“redux命令行模式”,其中dispatch比做分发器,这个形容很贴切。Dispatch方法就是接收action并将action里的信息分发给store和reducer,这里画了一个简单的图示以dispatch(action)
方式展示dispatch函数的执行过程。
const action = { type: 'ADD', payload: '***'}; dispatch(action);
除此以外dispatch函数的执行方法两种形式,其一是bindActionCreators(action)
方式,bindActionCreators函数在本文的后面也有说到。
const action = { type: 'ADD', payload: '***'}; const bindActionCreators = require('redux').bindActionCreators; bindActionCreators(action);
其二是dispatch action creator
方式,大多数项目中应用的都是这种方式。
function addTodo(text) { return { type: ADD, payload: text, } } store.dispatch(addTodo('***'));
每次执行dispatch,经过subscribe注册的listener都会被执行,当listener列表较多时listeners[i]()
都会被执行由此产生性能损耗,从工程师的角度更难定位到哪一个具体的reducer内的监听者被触发,这个时候须要一些辅助工具借助applyMiddleware
函数扩展中间件来帮助开发者。
import {applyMiddleware} from 'redux'; import thunk from 'redux-thunk'; const store = applyMiddleware([thunk])(createStore); const dispatch = store.dispatch;
Middleware is the suggested way to extend Redux with custom functionality3.
更全面具体一些的扩展就要说到redux中间件的概念,若是熟悉expressjs4或者koajs5,应该会对Middleware很熟悉,在redux中的中间件是一个高阶函数,通俗讲更多的是对现有dispatch函数进行扩展,其逻辑倾向于AOP6,意在将散布在各处的横切代码(cross-cutting code)以及一些被重复使用的功能性组件被重复使用。
import {applyMiddleware, compose, createStore} from 'redux'; import {routerMiddleware} from 'react-router-redux'; import thunkMiddleware from 'redux-thunk'; import {persistState} from 'redux-devtools'; import makeRootReducer from './reducers'; import DevTools from '../containers/DevTools'; export default (initialState = {}, history) => { ... const middleware = [thunkMiddleware, routerMiddleware(history), ...debugware]; const enhancers = []; ... enhancers.push(devToolsExtension()) ... const store = createStore( makeRootReducer(), initialState, compose( applyMiddleware(...middleware), ...enhancers ) ); ... store.replaceReducer(reducers(store.asyncReducers)) ... return store }
Subscribe从设计的角度来讲是一个订阅者,监听事件变化。
function subscribe(listener) { ... ensureCanMutateNextListeners() nextListeners.push(listener) return function unsubscribe() { ... ensureCanMutateNextListeners() var index = nextListeners.indexOf(listener) nextListeners.splice(index, 1) } }
getState是获取当前store的state(currentState);
function getState() { return currentState }
动态替换reducer函数
function replaceReducer(nextReducer) { ... currentReducer = nextReducer dispatch({ type: ActionTypes.INIT }) }
State对象存储在store状态树中,state只能经过dispatch(action)
来触发更新,更新逻辑由reducer来执行,须要注意的是当state变化时会返回全新的对象,而不是修改传入的参数。
这里示例的逻辑由四步分组成,第一部分是定义子路由,在子路由中引入用于收集reducer的回调函数,我命名为injectReducer,固然也能够命名其它的名字,如collectReducer、pushReducer等等均可以
import { injectReducer } from '../../store/reducers'; export default (store) => ({ path: 'login', getComponent (nextState, cb) { require.ensure([], (require) => { const LoginPage = require('./components/Login').default; const reducer = require('./extend/reducer').default; injectReducer(store, { key: 'login_reducer', reducer }); cb(null, LoginPage); }, 'login') } })
在第一部分中store和reducer已经被传入injectReducer中,第二部分就是injectReducer函数的内部逻辑。
import { combineReducers } from 'redux' import { routerReducer as router } from 'react-router-redux' export const makeRootReducer = (asyncReducers) => { return combineReducers({ router, ...asyncReducers }) }; export const injectReducer = (store, { key, reducer }) => { store.asyncReducers[key] = reducer; store.replaceReducer(makeRootReducer(store.asyncReducers)) }; export default makeRootReducer
InjectReducer函数内用replaceReducer方法将store从新计算,这里这样作的原因是第一部分的子路由是异步加载的,并非在服务器开始时直接加载完毕,而是随着用户在客户端不断操做页面异步更新reducer以及加载组件等信息。
import {applyMiddleware, compose, createStore} from 'redux'; ... import makeRootReducer from './reducers'; export default (initialState = {}, history) => { ... const store = createStore( makeRootReducer(), initialState, compose( applyMiddleware(...middleware), ...enhancers ) ); ... store.replaceReducer(reducers(store.asyncReducers)) ... return store }
第三部分是初始化store。
import CoreLayout from '../layouts/CoreLayout/components/CoreLayout'; import {Dashboard} from './module' export const createRoutes = (store) => ({ path : '/', component : CoreLayout, indexRoute: Home, getChildRoutes(location, cb) { cb(null, [ Dashboard(store) ]) } }); export default createRoutes
第四部分是初始化路由,按道理顺序应该是第四部分=>第三部分=>第一部分=>第二部分,可是若是考虑异步等信息的话,我我的认为按逻辑优先级应该是这样排比较好。
npm install --save normalizr
Store树对象或者组件自身state树对象实质上是JSON对象,因此在redux开发过程当中,为避免不一样数据之间相互引用或返回相互嵌套的值,可使用normalizr对state扁平化、范式化处理。
可变对象能够用Object.assign
或者lodash的cloneDeep
函数。
const assign = Object.assign || require('object.assign'); assign({}, state, { ADD: action.newState })
不可变对象(immutable state)是指在建立后不可再被修改的对象,它能够经过引用级的比对检查来提高渲染性能,在redux开发中通常会使用immutablejs实现不可变对象,须要注意的是immutablejs每次操做以后老是返回一个新的数据,原有的数据不会改变。
immutablejs经过结构共享来解决的数据拷贝时的性能问题,即当数据对象key->value
键值对被改变时,immutablejs会只clone
数据对象被改变对象节点的父节点以上的部分,其余保持不变,由此达到旧对象与immutablejs返回的新对象共享部分数据并提升性能。
测试:
Selector扩展组件,因为reselect带有缓存功能,因此使用它能够避免没必要要的selector计算
Action一样是一个javascript对象,一般包含type
等一些字段。
“Action Creator”是action的创造者,本质上就是一个函数,返回值是一个action,“Action Creator”能够是同步也能够是异步。
function add() { return { tyle: 'ADD' } } dispatch(add());
redux-thunks 和 redux-promise 分别是使用异步回调和 Promise 来解决异步 action 问题的。
若是直接用Fetch API,可能一些浏览器并不支持,因此仍是须要添加垫片isomorphic-fetch
function fetchDataAsync() { return function (dispatch) { fetch('/posttest', { method : 'POST', headers: { 'Content-Type': "application/json", 'Accept' : "application/json" }, body : JSON.stringify({item: 'text'}) }).then(res => { if (res.ok) { dispatch({type: LOGIN_REQUEST, loginRequest: true}); ... } }, e => { ... }); } }
bindActionCreators()
能够自动把多个action建立函数绑定到dispatch()
方法上。
借鉴store对reducer的封装(减小传入 state 参数)。能够对dispatch进行再一层封装,将多参数转化为单参数的形式,经 bindActionCreators包装事后的“Action Creator”造成了具备改变全局state数据的多个函数,将这些函数分发到各个地方,即能经过调用这些函数来改变全局的state。
var actionCreators = bindActionCreators ( actionCreators , store.dispatch ) ;
Reducer是一个javaScript函数,命名上也与Array.prototype.reduce()相像,函数签名为(previousState, action) => newState
,接受previousState和action两个参数,根据action.type
中携带的信息对previousState作出相应的处理,并返回一个新的state。另外在redux中一个action能够触发多个reducer,一个reducer中也能够包含多种“action.type”的处理,因此两者关系为多对多。。
import {SET_AUTH} from './actionType'; const assign = Object.assign || require('object.assign'); const initialState = { loggedIn : require('./action').default()(), }; const ACTION_HANDLERS = { [SET_AUTH] : (state, action) => assign({}, state, { loggedIn: action.newState }) }; export default function (state = initialState, action) { const handler = ACTION_HANDLERS[action.type]; return handler ? handler(state, action) : state }
combineReducers()
将调用一系列 reducer,并根据对应的 key 来筛选出 state 中的一部分数据给相应的 reducer,这样也意味着每个小的 reducer 将只能处理 state 的一部分数据,如:filterReducer 将只能处理及返回 state.filter 的数据,若是须要使用到其余 state 数据,那仍是须要为这类 reducer 传入整个 state。
import { combineReducers } from 'redux' ... combineReducers({ router, ...asyncReducers }) ...
React经过Context属性,能够将属性props直接给子component,无须经过props层层传递, Provider得到store而后将其传递给子元素。
export default class Provider extends Component { getChildContext() { return { store: this.store } } constructor(props, context) { super(props, context) this.store = props.store } componentWillReceiveProps(nextProps) { const { store } = this const { store: nextStore } = nextProps if (store !== nextStore) { warnAboutReceivingStore() } } render() { let { children } = this.props return Children.only(children) } } Provider.childContextTypes = { store: storeShape.isRequired }
Provider中的store能够在子组件中用contextTypes获取。
childrenComponent.contextTypes = { store: storeShape }
须要注意的是因为react-redux,咱们通常对绑定的组件称为Smart and Dumb Components
。
Location | Use React-Redux | To read data, they | To change data, they | |
---|---|---|---|---|
“Smart” Components | Top level, route handlers | Yes | Subscribe to Redux state | Dispatch Redux actions |
“Dumb” Components | Middle and leaf components | No | Read data from props | Invoke callbacks from props |
Provider将store放到context中,connect就能够获取store,使用store的方法,好比dispatch。其实没有被connect的组件经过声明contextTypes属性也是能够获取store,使用store的方法的,可是这个时候,若是使用dispatch修改了store的state,React-Redux并不能把修改后的state做为props给React组件,可能会致使UI和数据不一样步,因此这个时候必定要清楚本身在作什么。
import React, { Component, PropTypes } from 'react'; import { Router } from 'react-router'; import { Provider } from 'react-redux'; class AppContainer extends Component { static propTypes = { history: PropTypes.object.isRequired, routes: PropTypes.object.isRequired, store: PropTypes.object.isRequired }; render () { const { history, routes, store } = this.props; return ( <Provider store={store}> <div style={{ height: '100%' }}> <Router history={history} children={routes} /> </div> </Provider> ) } } export default AppContainer
npm i --save react-redux
Connect是由react-redux提供的一个高阶函数。源码中connect函数接收mapStateToProps、mapDispatchToProps、mergeProps、options
四个参数返回一个用于生产Component的函数wrapWithConnect,而后再将组件Component做为参数注入wrapWithConnect(WrappedComponent)
函数。
参数 | 描述 |
---|---|
mapStateToProps | 将state做为返回结果绑定到组件的props对象上 |
mapDispatchToProps | |
mergeProps | |
options |
export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) { ... return function wrapWithConnect(WrappedComponent) { ... } }
值得一说的是hoistStatics函数源于hoist-non-react-statics
第三方,做用是将原来组件中的元素拷贝到目标组件。在使用connect函数的时候直接在已声明的component后面引用connect。
import React, {Component} from 'react'; ... import {connect} from 'react-redux'; class Login extends Component { ... render() { ... } } ... export default connect(mapStateToProps, mapDispatchToProps)(Login)
Connect不仅为react组件提供store中的state数据及扩展dispatch方法,它还为定义的组件添加了一系列事件操做,这些事件的核心点就是store,而后能够在本身定义的组件内得到store。
constructor(){ //获取store this.store = props.store || context.store const storeState = this.store.getState() //把store的state做为组件的state,后面经过更新state更新组件 this.state = { storeState } //清除组件的状态,内部是一系列的标示还原 this.clearCache() }
Github源码 | 描述 |
---|---|
ducks-modular-redux | {ctionTypes, actions, reducer}规则解决方案 |
react-slingshot | |
saga-login-flow | |
login-flow | |
redux-saga | |
redux-auth-wrapper | |
dva | |
react-redux-tutorial | |
reduxjs doc | reduxjs中文档案 |
alloyteam:react-redux | React 数据流管理架构之 Redux 介绍 |
redux.js
文档中首页的一段话,对redux
特性的官方描述。 ↩ DOM
元素点击、页面路由切换等功能的操做行为,在redux中被称为action。 ↩ middleware
函数可以访问请求对象 req
、响应对象 res
以及应用程序的请求/响应循环中的下一个中间件middleware
函数。下一个中间件函数一般由名为next
的变量来表示。 ↩ OOP
)。 ↩