愿你坚持不懈,努力进步,进阶成自己理想的人

—— 2017.09, 写给3年后的自己

Redux学习笔记(二): 异步处理

一、异步action

简单的应用如todo应用,涉及的数据操作都是同步操作。但是一般应用开发中都不可避免异步操作,那么如何用redux处理异步数据流呢?

1、Action

调用异步API时,通常要考虑三个时刻:

  • 发起请求时
  • 请求成功
  • 请求失败

所以一般API调用过程中涉及三种action的dispatch:
1)通知reducer请求开始的action:通常会进行isFetching状态标记
2)通知reducer请求成功的action:通常标记isFetching,并且整合获取到的数据
3)通知reducer请求失败的action:可能会标记isFetching或者记录错误信息
这三种action的写法很简单,可以是用相同的type和不同的payload定义,也可以是不同的type方式,如:

// 方式一
{ type: 'FETCH_POSTS' }
{ type: 'FETCH_POSTS', status: 'error', error: '无操作' }
{ type: 'FETCH_POSTS', status: 'success', response: { ... } }
// 方式二
{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: '无操作' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }

2、同步Action创建函数

以一个内容聚合应用为例,我们可以先定义几个同步的action type和action创建函数
首先,用户可以切换不同的Tab来显示不同的内容:

// actions.js
export const SELECT_SUBREDDIT = 'SELECT_SUBREDDIT'
export function selectSubreddit(subreddit) {
    return {
        type: SELECT_SUBREDDIT,
        subreddit
    }
}
// 用户除了切换Tab,还可以通过点击“刷新”按钮来更新内容
export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'
export function invalidateSubreddit(subreddit) {
    return {
        type: INVALIDATE_SUBREDDIT,
        subreddit
    }
}

除了用户操作触发的action外,还有一些action是提供给网络请求控制用的,如:

// 当想要获取指定分类的帖子时,要dispatch一个REQUEST_POSTS action
export const REQUEST_POSTS = 'REQUEST_POSTS'
export function requestPosts(subreddit) {
    return {
        type: REQUEST_POSTS,
        subreddit
    }
}
// 当收到请求响应时,也需要dispatch一个RECEIVE_POSTS action
export const RECEIVE_POSTS = 'RECEIVE_POSTS'
export function receivePosts(subreddit, json) {
    return {
        type: RECEIVE_POSTS,
        subreddit,
        posts: json.data.children.map(child => child.data),
        receivedAt: Date.now()
    }
}

3、设计state结构

在写异步代码时,我们需要考虑更多的state。如在Web应用中,通常列表的展示是不可少的,所以我们首先要明确要显示哪些列表,从而在state中进行分别存储。以Reddit头条应用为例子,我们可以设计出如下的state

{
    selectedSubreddit: 'frontend',
    posts: {
        frontend: {
            isFetching: true,
            didInvalidate: false,
            items: []
        },
        reactjs: {
            isFetching: false,
            didInvalidate: false,
            lastUpdated: 1234567890000,
            items: [
                { id: 20, title: 'article A' },
                { id: 30, title: 'article B' }
            ]
        }
    }
}

对于以上结构,解释如下:
1)在posts中分开存储,是为了缓存考虑,从而方便在用户来回切换的时候可以立即显示,同时也可以在不必要的时候不请求数据
2)列表中采用isFetching字段是必要的,因为这样子可以给用户展示一个加载中的进度条。而didInvalidate则用来标记数据是否过期,lastUpdated则存放数据最后更新时间,items存放的则是列表数据本身。
3)实际应用中,还需要有fetchedPageCountnextPageUrl等分页相关的state

4、处理action

以上我们已经做完了准备工作:准备action创建函数、设计state,而接下来,就是开发reducer,为之后的与网络请求结合使用做准备,如下:

// reducers.js
import { combineReducers } from 'redux'
import {
    SELECT_SUBREDDIT, INVALIDATE_SUBREDDIT,
    REQUEST_POSTS, RECEIVE_POSTS
} from '../actions'

function selectedSubreddit(state = 'reactjs', action) {
    switch (action.type) {
        case SELECT_SUBREDDIT:
            return action.subreddit
        default:
            return state
    }
}

function posts(state = {
    isFetching: false,
    didInvalidate: false,
    items: []
}, action) {
    switch (action.type) {
        case INVALIDATE_SUBREDDIT:
            return {
                ...state,
                didInvalidate: true
            }
        case REQUEST_POSTS:
            return {
                ...state,
                isFetching: true,
                didInvalidate: false
            }
        case RECEIVE_POSTS:
            return {
                ...state,
                isFetching: false,
                didInvalidate: false,
                items: action.posts,
                lastUpdated: action.receivedAt
            }
        default:
            return state
    }
}

function postsBySubreddit(state = {}, action) {
    switch (action.type) {
        case INVALIDATE_SUBREDDIT:
        case REQUEST_POSTS:
        case RECEIVE_POSTS:
            return {
                ...state,
                [action.subreddit]: posts(state[action.subreddit], action)
            }
        default:
            return state
    }
}

const rootReducer = combineReducers({
    selectedSubreddit
    posts
})

export default rootReducer

5、异步Action创建函数

将同步action创建函数和网络请求结合起来的标准做法是使用ReduxThunkMiddleware。通过使用这个middleware,action创建函数除了返回action对象外,还可以返回函数,此时的action创建函数就成了thunk
当action创建函数返回的是一个函数时,这个函数就会被ReduxThunkMiddleware执行,并且这个函数不需要保持纯净:可以带有副作用、可以执行异步API请求、可以dispatch action,如:

// actions.js
import fetch from 'isomorphic-fetch'

export function fetchPosts(subreddit) {
    // ThunkMiddleware会知道如何处理函数,
    // 所以此处可以把dispatch方法通过参数的形式传给函数
    // 从而它自己也可以dispatch action
    return function(dispatch) {
        dispatch(requestPosts(subreddit))
        // ThunkMiddleware调用的函数可以有返回值,
        // 这个返回值会被当做`dispatch`方法的返回值传递
        return fetch(`http://www.subreddit.com/r/${subreddit}.json`)
            .then(response => response.json())
            .then(json => dispatch(receivePosts(subreddit, json)))
    }
}

6、applyMiddleware()

如何在dispatch机制中引入ReduxThunkMiddleware呢?做法是:采用applyMiddleware(),如:
index.js

import thunkMiddleware from 'redux-thunk'
import createLogger from 'redux-logger'
import { createStore, applyMiddleware } from 'redux'
import { selectSubreddit, fetchPosts } from './actions'
import rootReducer from './reducers'

const loggerMiddleware = createLogger()
const store = createStore(
    rootReducer,
    applyMiddleware(
        thunkMiddleware,
        loggerMiddleware
    )
)

store.dispatch(selectSubreddit('reactjs'))
store.dispatch(fetchPosts('reactjs')).then(() => {
    console.log(store.getState())
    // 这里便用到了`ThunkMiddleware调用的函数可以的返回值`
})

需要注意的是:
1)thunk函数里可以再dispatch action
2)异步action创建函数对于做服务端渲染也是很方便的。我们可以创建一个store,然后dispatch一个异步action创建函数,这个异步action创建函数可以再dispatch另一个异步action创建函数来为应用的一整块请求数据,然后在Promise完成后再render界面,从而在render之前,store里便已经具备了需要的state


二、异步数据流

默认情况下,createStore()创建的Redux Store只支持同步数据流。而我们可以使用applyMiddleware()来增强createStore(),如redux-thunkredux-promise一类支持异步的middleware都包装了storedispatch()方法,从而我们可以dispatch一些除了action外的内容
需要注意的是:
当middleware链中最后一个middleware开始dispatch action时,这个action必须是一个普通对象。即:可以使用任意多异步的middleware来做想做的是全,但是最后必须要用一个普通对象作为被dispatch的action,来将处理流程带回同步方式