react的FLUX数据流一直搞不清楚,他不像Angular
的双向数据绑定,做一个model
获取数据,然后通过controller
来管理view上
的数据显示就可以了。单项数据流引入了太多的概念,state
、action
、reducer
、dispatch
。就算看的懂图,也不一定能coding出来。
不过我总算先搞定了Redux
。
keywords
store
reducer
action
dispatch
connect
router
middleware
thunk
Basic Usage
1st 实现action方法
export const addDeck = name => ({ type: 'ADD_DECK',data: name });
2nd 根据action方法创建reducer方法
export const showBack = (state,action) => { switch(action.type) { case 'SHOW_BACK': return action.data || false; default: return state || false; } };
3rd 根据reducer方法创建store
const store = createStore(combineReducers(reducers));
store.subscribe()
方法设置监听函数,一旦 State 发生变化,就自动执行这个函数。
store.subscribe(listener);
显然,只要把 View 的更新函数(对于 React 项目,就是组件的render方法或setState方法)放入listen,就会实现 View 的自动渲染。
store.subscribe方法返回一个函数,调用这个函数就可以解除监听。
let unsubscribe = store.subscribe(() => console.log(store.getState()) ); unsubscribe();
4th 引入react-redux的<Provider>,导入store
<Provider store={store}> {...} </Provider>
5th react组件中通过connect方法绑定store和dispatch。
const mapStateToProps = (newTalks) => ({ newTalks }); const mapDispatchToProps = dispatch => ({ testFunc: () => dispatch(updataTalkLists(1)),receiveData: () => dispatch(receiveData()) }); export default connect(mapStateToProps,mapDispatchToProps)(MainPage);
6th this.props中直接调用action方法。
this.props.receiveData
With react-router
结合router使用时需要有2步。
1st 绑定routing到reducer上
import { syncHistoryWithStore,routerReducer } from 'react-router-redux'; import * as reducers from './redux/reducer'; reducers.routing = routerReducer; const store = createStore(combineReducers(reducers));
2nd 使用syncHistoryWithStore绑定store和browserHistory
const history = syncHistoryWithStore(browserHistory,store); <Provider store={store}> <Router history={history}> {routes} </Router> </Provider>
Async
类似 Express 或 Koa 框架中的中间件。它提供的是位于 action 被发起之后,到达 reducer 之前的扩展。
中间件的设计使用了非常多的函数式编程的思想,包括:高阶函数,复合函数,柯里化和ES6语法,源码仅仅20行左右。
项目中主要使用了三个中间件,分别解决不同的问题。
thunkMiddleware:处理异步Action
apiMiddleware:统一处理API请求。一般情况下,每个 API 请求都至少需要 dispatch 三个不同的 action(请求前、请求成功、请求失败),通过这个中间件可以很方便处理。
loggerMiddleware:开发环境调试使用,控制台输出应用state日志
实现action异步操作,必须要引入middleware。我这里用了applyMiddleware(thunkMiddleware)
组件,也可以用其他的。
1st 创建store是引入Middleware
import thunkMiddleware from 'redux-thunk'; import { createStore,combineReducers,applyMiddleware } from 'redux'; const store = createStore(combineReducers(reducers),applyMiddleware(thunkMiddleware));
2nd 创建一个可以执行dispacth的action
这也是中间件的作用所在。
export const receiveData = data => ({ type: 'RECEIVE_DATA',data: data }); export const fetchData = () => { return dispatch => { fetch('/api/data') .then(res => res.json()) .then(json => dispatch(receiveData(json))); }; };
3rd 组件中对异步的store元素有相应的判断操作。
React组件会在store值发生变化时自动调用render()方法,更新异步数据。但是我们同样也需要处理异步数据没有返回或者请求失败的情况。否则渲染会失败,页面卡住。
if(!data.newTalks) { return(<div/>); }
相关知识
Store的实现
Store提供了3个方法
import { createStore } from 'redux'; let { subscribe,//监听store变化 dispatch,//调用action方法 getState //返回当前store } = createStore(reducer);
下面是create方法的一个简单实现
const createStore = (reducer) => { let state; let listeners = []; const getState = () => state; const dispatch = (action) => { state = reducer(state,action); listeners.forEach(listener => listener()); }; const subscribe = (listener) => { listeners.push(listener); return () => { listeners = listeners.filter(l => l !== listener); } }; dispatch({}); return { getState,dispatch,subscribe }; };
combineReducer的简单实现
const combineReducers = reducers => { return (state = {},action) => { return Object.keys(reducers).reduce( (nextState,key) => { nextState[key] = reducers[key](state[key],action); return nextState; },{} ); }; };
中间件
const store = createStore( reducer,initial_state,applyMiddleware(thunk,promise,logger) );
applyMiddlewares的实现,它是将所有中间件组成一个数组,依次执行
export default function applyMiddleware(...middlewares) { return (createStore) => (reducer,preloadedState,enhancer) => { var store = createStore(reducer,enhancer); var dispatch = store.dispatch; var chain = []; var middlewareAPI = { getState: store.getState,dispatch: (action) => dispatch(action) }; chain = middlewares.map(middleware => middleware(middlewareAPI)); dispatch = compose(...chain)(store.dispatch); return {...store,dispatch} } }
上面代码中,所有中间件被处理后得到一个数组保存在chain中。之后将chain传给compose,并将store.dispatch传给返回的函数。。可以看到,中间件内部(middlewareAPI)可以拿到getState和dispatch这两个方法。
那么在这里面做了什么呢?我们再看compose的实现:
export default function compose(...funcs) { if (funcs.length === 0) { return arg => arg } else { const last = funcs[funcs.length - 1] const rest = funcs.slice(0,-1) return (...args) => rest.reduceRight((composed,f) => f(composed),last(...args)) } }
compose中的核心动作就是将传进来的所有函数倒序(reduceRight)进行如下处理:
(composed,f) => f(composed)
我们知道Array.prototype.reduceRight是从右向左累计计算的,会将上一次的计算结果作为本次计算的输入。再看看applyMiddleware中的调用代码:
dispatch = compose(...chain)(store.dispatch)
compose函数最终返回的函数被作为了dispatch函数,结合官方文档和代码,不难得出,中间件的定义形式为:
function middleware({dispatch,getState}) { return function (next) { return function (action) { return next(action); } } } 或 middleware = (dispatch,getState) => next => action => { next(action); }
也就是说,redux的中间件是一个函数,该函数接收dispatch和getState作为参数,返回一个以dispatch为参数的函数,这个函数的返回值是接收action为参数的函数(可以看做另一个dispatch函数)。在中间件链中,以dispatch为参数的函数的返回值将作为下一个中间件(准确的说应该是返回值)的参数,下一个中间件将它的返回值接着往下一个中间件传递,最终实现了store.dispatch在中间件间的传递。
redux-promise中间件
既然 Action Creator 可以返回函数,当然也可以返回其他值。另一种异步操作的解决方案,就是让 Action Creator 返回一个 Promise 对象。
写法一,返回值是一个 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() }); });
import { createAction } from 'redux-actions'; class AsyncApp extends Component { componentDidMount() { const { dispatch,selectedPost } = this.props // 发出同步 Action dispatch(requestPosts(selectedPost)); // 发出异步 Action dispatch(createAction( 'FETCH_POSTS',fetch(`/some/API/${postTitle}.json`) .then(response => response.json()) )); }
上面代码中,第二个dispatch方法发出的是异步 Action,只有等到操作结束,这个 Action 才会实际发出。注意,createAction的第二个参数必须是一个 Promise 对象。
redux-promise的源码
export default function promiseMiddleware({ dispatch }) { return next => action => { if (!isFSA(action)) { return isPromise(action) ? action.then(dispatch) : next(action); } return isPromise(action.payload) ? action.payload.then( result => dispatch({ ...action,payload: result }),error => { dispatch({ ...action,payload: error,error: true }); return Promise.reject(error); } ) : next(action); }; }
从上面代码可以看出,如果 Action 本身是一个 Promise,它 resolve 以后的值应该是一个 Action 对象,会被dispatch方法送出(action.then(dispatch)),但 reject 以后不会有任何动作;如果 Action 对象的payload属性是一个 Promise 对象,那么无论 resolve 和 reject,dispatch方法都会发出 Action。
mapStateToProps()
mapStateToProps是一个函数。它的作用就是像它的名字那样,建立一个从(外部的)state对象到(UI 组件的)props对象的映射关系
mapStateToProps会订阅 Store,每当state更新的时候,就会自动执行,重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染。
mapStateToProps的第一个参数总是state对象,还可以使用第二个参数,代表容器组件的props对象。
使用ownProps作为参数后,如果容器组件的参数发生变化,也会引发 UI 组件重新渲染。
connect方法可以省略mapStateToProps参数,那样的话,UI 组件就不会订阅Store,就是说 Store 的更新不会引起 UI 组件的更新。
// 容器组件的代码 // <FilterLink filter="SHOW_ALL"> // All // </FilterLink> const mapStateToProps = (state,ownProps) => { return { active: ownProps.filter === state.visibilityFilter } }
mapDispatchToProps()
mapDispatchToProps是connect函数的第二个参数,用来建立 UI 组件的参数到store.dispatch方法的映射。也就是说,它定义了哪些用户的操作应该当作 Action,传给 Store。它可以是一个函数,也可以是一个对象。
mapDispatchToProps
作为函数,应该返回一个对象,该对象的每个键值对都是一个映射,定义了 UI 组件的参数怎样发出 Action。
const mapDispatchToProps = ( dispatch,ownProps ) => { return { onClick: () => { dispatch({ type: 'SET_VISIBILITY_FILTER',filter: ownProps.filter }); } }; }
如果mapDispatchToProps
是一个对象,它的每个键名也是对应 UI 组件的同名参数,键值应该是一个函数,会被当作 Action creator
,返回的 Action
会由 Redux
自动发出。
const mapDispatchToProps = { onClick: (filter) => { type: 'SET_VISIBILITY_FILTER',filter: filter }; }
<Provider>
组件
React-Redux 提供Provider组件,可以让容器组件拿到state,它的原理是React组件的context属性,请看源码。
class Provider extends Component { getChildContext() { return { store: this.props.store }; } render() { return this.props.children; } } Provider.childContextTypes = { store: React.PropTypes.object }
上面代码中,store放在了上下文对象context上面。然后,子组件就可以从context拿到store,代码大致如下。
class VisibleTodoList 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(); // ... } } VisibleTodoList.contextTypes = { store: React.PropTypes.object }
redux-thunk
我们知道,异步调用什么时候返回前端是无法控制的。对于redux这条严密的数据流来说,如何才能做到异步呢。redux-thunk的基本思想就是通过函数来封装异步请求,也就是说在actionCreater中返回一个函数,在这个函数中进行异步调用。我们已经知道,redux中间件只关注dispatch函数的传递,而且redux也不关心dispatch函数的返回值,所以只需要让redux认识这个函数就可以了。
看了一下redux-thunk的源码:
function createThunkMiddleware(extraArgument) { return ({ dispatch,getState }) => next => action => { if (typeof action === 'function') { return action(dispatch,getState,extraArgument); } return next(action); }; } const thunk = createThunkMiddleware(); thunk.withExtraArgument = createThunkMiddleware; export default thunk;
这段代码跟上面我们看到的中间件没有太大的差别,唯一一点就是对action做了一下如下判断:
if (typeof action === 'function') { return action(dispatch,extraArgument); }
也就是说,如果发现actionCreater传过来的action是一个函数的话,会执行一下这个函数,并以这个函数的返回值作为返回值。前面已经说过,redux对dispatch函数的返回值不是很关心,因此此处也就无所谓了。
这样的话,在我们的actionCreater中,我们就可以做任何的异步调用了,并且返回任何值也无所谓,所以我们可以使用promise了:
function actionCreate() { return function (dispatch,getState) { // 返回的函数体内自由实现。。。 Ajax.fetch({xxx}).then(function (json) { dispatch(json); }) } }
最后还需要注意一点,由于中间件只关心dispatch的传递,并不限制你做其他的事情,因此我们最好将redux-thunk放到中间件列表的首位,防止其他中间件中返回异步请求。