一、创建同步Action
Action是数据从应用传递到 store/state 的载体,也是开启一次完成数据流的开始
普通的action对象
1 2 3 4 5 6
| const action = { type:'ADD_TODO', name:'poetries' }
dispatch(action)
|
封装action creator
1 2 3 4 5 6 7 8
| function actionCreator(data){ return { type:'ADD_TODO', data:data } }
dispatch(actionCreator('poetries'))
|
bindActionCreators合并
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| function a(name,id){ reurn { type:'a', name, id } } function b(name,id){ reurn { type:'b', name, id } }
let actions = Redux.bindActionCreators({a,b},store.dispatch)
actions.a('poetries','id001') actions.b('jing','id002')
|
action创建的标准
在Flux的架构中,一个Action要符合 FSA(Flux Standard Action) 规范,需要满足如下条件
- 是一个纯文本对象
- 只具备
type 、payload、error 和 meta中的一个或者多个属性。type 字段不可缺省,其它字段可缺省
- 若
Action 报错,error 字段不可缺省,切必须为 true
payload 是一个对象,用作Action携带数据的载体
标准action示例
- A basic Flux Standard Action:
1 2 3 4 5 6
| { type: 'ADD_TODO', payload: { text: 'Do something.' } }
|
- An FSA that represents an error, analogous to a rejected Promise
1 2 3 4 5
| { type: 'ADD_TODO', payload: new Error(), error: true }
|
https://github.com/acdlite/flux-standard-action
- 可以采用如下一个简单的方式检验一个
Action是否符合FSA标准
1 2 3 4
| let isFSA = Object.keys(action).every((item)=>{ return ['payload','type','error','meta'].indexOf(item) > -1 })
|
二、创建异步action的多种方式
最简单的方式就是使用同步的方式来异步,将原来同步时一个action拆分成多个异步的action的,在异步开始前、异步请求中、异步正常返回(异常)操作分别使用同步的操作,从而模拟出一个异步操作了。这样的方式是比较麻烦的,现在已经有redux-saga等插件来解决这些问题了
异步action的实现方式一:setTimeout
redux-thunk中间处理解析
1 2 3 4 5 6 7 8 9 10
| function thunkAction(data) { reutrn (dispatch)=>{ setTimeout(function(){ dispatch({ type:'ADD_TODO', data }) },3000) } }
|
异步action的实现方式二:promise实现异步action
redux-promise中间处理这种action
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function promiseAction(name){ return new Promise((resolve,reject) => { setTimeout((param)=>{ resolve({ type:'ADD_TODO', name }) },3000) }).then((param)=>{ dispatch(action("action2")) return; }).then((param)=>{ dispatch(action("action3")) }) }
|
三、redux异步流程
image.png
- 首先发起一个action,然后通过中间件,这里为什么要用中间件呢,因为这样
dispatch的返回值才能是一个函数。
- 通过
store.dispatch,将状态的的改变传给store的小弟reducer,reducer根据action的改变,传递新的状态state。
- 最后将所有的改变告诉给它的大哥,
store。store保存着所有的数据,并将数据注入到组件的顶部,这样组件就可以获得它需要的数据了
四、Redux异步方案选型
redux-thunk
Redux本身只能处理同步的Action,但可以通过中间件来拦截处理其它类型的action,比如函数(Thunk),再用回调触发普通Action,从而实现异步处理
- 发送异步的
action其实是被中间件捕获的,函数类型的action就被middleware捕获。至于怎么定义异步的action要看你用哪个中间件,根据他们的实例来定义,这样才会正确解析action
Redux 本身不处理异步行为,需要依赖中间件。结合 redux-actions 使用,Redux 有两个推荐的异步中间件
redux-thunk
redux-promise
redux-thunk 的源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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 creator 需要返回一个函数给 redux-thunk 进行调用,示例如下
1 2 3 4 5 6 7 8 9 10 11
| export let addTodoWithThunk = (val) => async (dispatch, getState)=>{
let value = await Promise.resolve(val + ' thunk'); dispatch({ type:CONSTANT.ADD_TO_DO_THUNK, payload:{ value } }); };
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| const GET_DATA = 'GET_DATA', GET_DATA_SUCCESS = 'GET_DATA_SUCCESS', GET_DATA_FAILED = 'GET_DATA_FAILED';
const getDataAction = (id) => (dispatch, getState) => { dispatch({ type: GET_DATA, payload: id }) api.getData(id) .then(response => { dispatch({ type: GET_DATA_SUCCESS, payload: response }) }) .catch(error => { dispatch({ type: GET_DATA_FAILED, payload: error }) }) } }
const reducer = (oldState, action) => { switch(action.type) { case GET_DATA : return oldState; case GET_DATA_SUCCESS : return successState; case GET_DATA_FAILED : return errorState; } }
|
这已经是最简单的场景了,请注意:我们甚至还没写一行业务逻辑,如果每个异步处理都像这样,重复且无意义的工作会变成明显的阻碍
- 另一方面,像
GET_DATA_SUCCESS、GET_DATA_FAILED这样的字符串声明也非常无趣且易错
上例中,GET_DATA这个action并不是多数场景需要的
redux-promise
由于redux-thunk写起来实在是太麻烦了,社区当然会有其它轮子出现。redux-promise则是其中比较知名的
- 它自定义了一个
middleware,当检测到有action的payload属性是Promise对象时,就会
- 若
resolve,触发一个此action的拷贝,但payload为promise的value,并设status属性为"success"
- 若
reject,触发一个此action的拷贝,但payload为promise的reason,并设status属性为"error"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| const GET_DATA = 'GET_DATA';
const getData = function(id) { return { type: GET_DATA, payload: api.getData(id) } }
function reducer(oldState, action) { switch(action.type) { case GET_DATA: if (action.status === 'success') { return successState } else { return errorState } } }
|
redux-promise为了精简而做出的妥协非常明显:无法处理乐观更新
场景解析之:乐观更新
多数异步场景都是悲观更新的,即等到请求成功才渲染数据。而与之相对的乐观更新,则是不等待请求成功,在发送请求的同时立即渲染数据
- 由于乐观更新发生在用户操作时,要处理它,意味着必须有action表示用户的初始动作
- 在上面
redux-thunk的例子中,我们看到了GET_DATA, GET_DATA_SUCCESS、GET_DATA_FAILED三个action,分别表示初始动作、异步成功和异步失败,其中第一个action使得redux-thunk具备乐观更新的能力
- 而在
redux-promise中,最初触发的action被中间件拦截然后过滤掉了。原因很简单,redux认可的action对象是 plain JavaScript objects,即简单对象,而在redux-promise中,初始action的payload是个Promise
redux-promise-middleware
redux-promise-middleware相比redux-promise,采取了更为温和和渐进式的思路,保留了和redux-thunk类似的三个action
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| const GET_DATA = 'GET_DATA', GET_DATA_PENDING = 'GET_DATA_PENDING', GET_DATA_FULFILLED = 'GET_DATA_FULFILLED', GET_DATA_REJECTED = 'GET_DATA_REJECTED';
const getData = function(id) { return { type: GET_DATA, payload: { promise: api.getData(id), data: id } } }
const reducer = function(oldState, action) { switch(action.type) { case GET_DATA_PENDING : return oldState; case GET_DATA_FULFILLED : return successState; case GET_DATA_REJECTED : return errorState; } }
|
五、redux异步操作代码演示
action/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import fetch from 'isomorphic-fetch' export const RECEIVE_POSTS = 'RECEIVE_POSTS'
function receivePosts(reddit, json) { return { type: RECEIVE_POSTS, reddit: reddit, posts: json.data.children.map(child =>child.data) } }
function fetchPosts(subreddit) {
return function (dispatch) { return fetch(`http://www.subreddit.com/r/${subreddit}.json`) .then(response => response.json()) .then(json => dispatch(receivePosts(subreddit, json)) ) } }
export function fetchPostsIfNeeded(subreddit) {
return (dispatch, getState) => {
return dispatch(fetchPosts(subreddit))
} }
|
fetchPostsIfNeeded这里就是一个中间件。redux-thunk会拦截fetchPostsIfNeeded这个action,会先发起数据请求,如果成功,就将数据传给action从而到达reducer那里
reducers/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import { combineReducers } from 'redux' import { RECEIVE_POSTS } from '../actions'
function posts(state = { items: [] }, action) { switch (action.type) {
case RECEIVE_POSTS: return Object.assign({}, state, { items: action.posts }) default: return state } }
const rootReducer = combineReducers({ postsByReddit })
export default rootReducer
|
这个跟正常的reducer差不多。判断action的类型,从而根据action的不同类型,返回不同的数据。这里将数据存储在了items这里。这里的reducer只有一个。最后结合成rootReducer,传给store
store/configureStore.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import { createStore, applyMiddleware } from 'redux' import thunkMiddleware from 'redux-thunk' import createLogger from 'redux-logger' import rootReducer from '../reducers'
const createStoreWithMiddleware = applyMiddleware( thunkMiddleware, createLogger() )(createStore)
export default function configureStore(initialState) { const store = createStoreWithMiddleware(rootReducer, initialState)
if (module.hot) { module.hot.accept('../reducers', () => { const nextRootReducer = require('../reducers') store.replaceReducer(nextRootReducer) }) }
return store }
|
- 我们是如何在
dispatch 机制中引入 Redux Thunk middleware 的呢?
我们使用了applyMiddleware()
- 通过使用指定的
middleware,action creator 除了返回 action 对象外还可以返回函数
- 这时,这个
action creator 就成为了 thunk
界面上的调用:在containers/App.js
1 2 3 4 5 6
| componentDidMount() { const { dispatch} = this.props dispatch(fetchPostsIfNeeded('frontend')) }
|
改变状态的时候也是需要通过dispatch来传递的
- 数据的获取是通过
provider,将store里面的数据注入给组件。让顶级组件提供给他们的子孙组件调用。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| import 'babel-core/polyfill' import React from 'react' import { render } from 'react-dom' import { Provider } from 'react-redux' import App from './containers/App' import configureStore from './store/configureStore' const store = configureStore() render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') )
|
这样就完成了redux的异步操作。其实最主要的区别还是action里面还有中间件的调用,其他的地方基本跟同步的redux差不多的。搞懂了中间件,就基本搞懂了redux的异步操作
image.png