Asynchronous Action
文章目录

tags: 异步, Asynchronous, Action, Thunk, 回调

Redux: Asynchronous Action (异步Action)

处理异步的时候, 有两个非常关键的时刻:

  1. 发起请求的时刻
  2. 和接收到响应的时刻(也可能是超时)

处理异步请求的三种Action

为了处理异步, 每个API请求必须dispatch至少三种action:

  1. 通知 reducer 请求开始的 action

    对于这种 action,reducer 可能会切换一下 state 中的 isFetching 标记。以此来告诉 UI 来显示加载界面。

  2. 通知 reducer 请求成功的 action

    对于这种 action,reducer 可能会把接收到的新数据合并到 state 中,并重置 isFetching。UI 则会隐藏加载界面,并显示接收到的数据。

  3. 通知 reducer 请求失败的 action

    对于这种 action,reducer 可能会重置 isFetching。另外,有些 reducer 会保存这些失败信息,并在 UI 里显示出来。

三种Action的实现

当然,这个只是推荐的实现办法, 最终还是要根据项目需求进行修改

为了区分这三种 action,可能在 action 里添加一个专门的 status 字段作为标记位:

1
2
3
{ type: 'FETCH_POSTS' }
{ type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
{ type: 'FETCH_POSTS', status: 'success', response: { ... } }

又或者为它们定义不同的 type:

1
2
3
{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }

将Action和网络请求链接起来

这里通过redux-thunk实现, 当然还有很多种其他的实现办法

首先要了解一下Thunk

Thunk?

Thunk是一个类似于外挂的middleware

可以在某些请求之上进行一些外的处理

如何创建一个能被Thunk处理的Action

引用这个库: redux-thunk

  • 实现方法: 当action 创建函数返回函数时,这个函数会被 Redux Thunk middleware执行
  • 这个函数并不需要保持纯净;它还可以带有副作用,包括执行异步 API 请求。这个函数还可以 dispatch action,就像 dispatch 前面定义的同步 action 一样。
  • 甚至这个Action可以返回对另一个Action的Dispatch, 类似于回调的样子

所以很简单, 让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
29
30
31
32
33
34
35
36
37
38
39
40
// 来看一下我们写的第一个 thunk action 创建函数!
// 虽然内部操作不同,你可以像其它 action 创建函数 一样使用它:
// store.dispatch(fetchPosts('reactjs'))

export function fetchPosts(subreddit) {

// Thunk middleware 知道如何处理函数。
// 这里把 dispatch 方法通过参数的形式传给函数,
// 以此来让它自己也能 dispatch action。

return function (dispatch) {

// 首次 dispatch:更新应用的 state 来通知
// API 请求发起了。

dispatch(requestPosts(subreddit))

// thunk middleware 调用的函数可以有返回值,
// 它会被当作 dispatch 方法的返回值传递。

// 这个案例中,我们返回一个等待处理的 promise。
// 这并不是 redux middleware 所必须的,但这对于我们而言很方便。

return fetch(`http://www.subreddit.com/r/${subreddit}.json`)
.then(
response => response.json(),
// 不要使用 catch,因为会捕获
// 在 dispatch 和渲染中出现的任何错误,
// 导致 'Unexpected batch number' 错误。
// https://github.com/facebook/react/issues/6895
error => console.log('An error occurred.', error)
)
.then(json =>
// 可以多次 dispatch!
// 这里,使用 API 请求结果来更新应用的 state。

dispatch(receivePosts(subreddit, json))
)
}
}

详细实现

createStore() init的时候多传一个参数

默认情况下,createStore() 所创建的 Redux store 没有使用 middleware

我们多传一个参数,将中间件一起传进去:

1
2
3
4
5
6
7
8
9
10
11
12
13
import thunk from 'redux-thunk'
import { createLogger } from 'redux-logger'
import reducer from './reducers'

const middleware = [ thunk ]
if (process.env.NODE_ENV !== 'production') {
middleware.push(createLogger())
}

const store = createStore(
reducer,
applyMiddleware(...middleware)
)

另一个例子:

1
2
3
4
5
6
7
const store = createStore(
rootReducer,
applyMiddleware(
thunkMiddleware, // lets us dispatch() functions
loggerMiddleware // neat middleware that logs actions
)
)

dispatch部分

和普通Action一样, dispatch一个Action:

1
2
3
4
componentDidMount() {
const { dispatch, selectedSubreddit } = this.props
dispatch(fetchPostsIfNeeded(selectedSubreddit))
}

比如这个地方的Action是fetchPostsIfNeeded()

如果是普通的Action, 它应该返回一个Obj作为state结果

但是这里我们要通过Thunk进行处理, 因此返回一个函数

返回的函数里面还可以返回另一个dispatch, 这个dispatch又可以返回另一个函数, 最终只要再返回函数了, 那么就结束回调

  1. action: fetchPosts里面返回另一个函数, 这个函数里面执行了
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
/* 0. 首先componentDidMount()里面dispatch第一个action: fetchPostsIfNeeded */
/* 1. fetchPostsIfNeeded 里面返回一个函数, 这个函数返回另一个dispatch, 这个dispatch会dispatch另一个action: fetchPost */
export const fetchPostsIfNeeded = subreddit => (dispatch) => {

...(some validation)

return dispatch(fetchPosts(subreddit))
}


/* 2. 同样也会返回一个函数,这个函数里面会处理一次dispatch(并不是返回), 返回进行http请求然后返回另一个Promise, Primise的最终回调会提交action: receivePosts */
const fetchPosts = subreddit => dispatch => {
dispatch(requestPosts(subreddit));
return fetch(`https://www.reddit.com/r/${subreddit}.json`)
.then(response => response.json())
.then(json => dispatch(receivePosts(subreddit, json)))
}

// 3. receivePosts就是简单的state处理了
const receivePosts = (subreddit, json) => ({
type: RECEIVE_POSTS,
subreddit,
posts: json.data.children.map(child => child.data),
receivedAt: Date.now()
})

Middleware

可以明显的看到实现Asynchronous Action是基于Middleware实现的

当然Middleware可以自己写:

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
/**
* 记录所有被发起的 action 以及产生的新的 state。
*/
const logger = store => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}

/**
* 记录错误日志
*/

const crashReporter = store => next => action => {
try {
return next(action)
} catch (err) {
console.error('Caught an exception!', err)
Raven.captureException(err, {
extra: {
action,
state: store.getState()
}
})
throw err
}
}

这堆代码看起来很见鬼, 特别是链式调用

换成named function的形式可能会好理解一些(但是当然不推荐用下面这个老方法):

1
2
3
4
5
6
7
8
9
10
11
12
13
// 1. middleware 的名字叫做 logger, 需要将store传进去
function logger(store) {
// 2. next就是thunk返回的函数, 就是下一步需要操作的事情
return function wrapDispatchToAddLogging(next) {
// 3. 在这个函数里面就加了一个外挂, 不但执行了那个action还在执行前执行后,分别输出了其他相关信息
return function dispatchAndLog(action) {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
}
}

然后是将它们引用到 Redux store 中:

1
2
3
4
5
6
7
8
import { createStore, combineReducers, applyMiddleware } from 'redux'

let todoApp = combineReducers(reducers)
let store = createStore(
todoApp,
// applyMiddleware() 告诉 createStore() 如何处理中间件
applyMiddleware(logger, crashReporter)
)

再然后任何发送到 store 的 action 都会经过 loggercrashReporter

1
2
// 将经过 logger 和 crashReporter 两个 middleware!
store.dispatch(addTodo('Use Redux'))