redux异步数据流(一)

Posted by XuBaoshi on October 12, 2017

redux异步数据流(一)#

前言

redux作为状态管理的解决方案,运行时大体流程如下:用户触发某个操作,对应的操作会触发(dispatch)action(由action creator执行返回),reducer根据action类型返回新的state,view重新渲染。需要注意的是redux原始的dispacth方法接受的action必须是一个对象,redux的dispatch方法源码如下:

/img/redux-async/dispatch.PNG

假设有一个这样的场景,一个组件点击按钮查询展示来自服务端的列表信息,流程如下:

  • 点击按钮请求开始,页面显示“正在查询。。。
  • 如果请求成功,页面显示查询到的数据列表。
  • 如果请求失败,页面显示“出现错误,请重试。。。

页面显示的依据均是由redux状态判断,故由此可以得出以下三个action。

  • 通知 reducer 请求开始的 action。
  • 通知 reducer 请求成功结束的 action。
  • 通知 reducer 请求失败的 action。

action creator部分及reducer代码如下:

// constants
const FETCH_CNODE_LIST = 'FETCH_CNODE_LIST';
const FETCH_CNODE_FAILURE = 'FETCH_CNODE_FAILURE';
const FETCH_CNODE_SUCCESS = 'FETCH_CNODE_SUCCESS';

// reducer
const listReducer = (state = { isFetching: false, hasError: false, data: [] }, action) => {
    switch (action.type) {
        case FETCH_CNODE_LIST:
            return Object.assign({}, state, { isFetching: true, hasError: false });
        case FETCH_CNODE_FAILURE:
            return Object.assign({}, state, { hasError: true, isFetching: false });
        case FETCH_CNODE_SUCCESS:
            return Object.assign({}, state, { data: action.data, isFetching: false, hasError: false });
        default:
            return state
    }
}

// action creator
const beginFectch = () => {
    return {
        type: FETCH_CNODE_LIST
    }
}
const fectchFail = () => {
    return {
        type: FETCH_CNODE_FAILURE
    }
}
const fectchSuccess = (data) => {
    return {
        type: FETCH_CNODE_SUCCESS,
        data: data
    }
}

由上可以看出action creator所返回的都是对象。

// CnodeList component
class CnodeList extends React.Component {
    constructor(props) {
        super(props);
        this.getNodeList = this.getNodeList.bind(this);
    }
    render() {
        const { cnode } = this.props;
        var str = '';
        if (cnode.isFetching) {
            str = '正在查询。。。。';
        }
        else if (cnode.hasError) {
            str = '出现错误,请重试。。。';
        } else {
            if (cnode.data && cnode.data.length > 0) {
                str = cnode.data.map(obj =>
                    <li key={obj.id}>
                        <strong>id:</strong><span style=marginRight>{obj.id}</span>
                        <strong>author_id:</strong><span>{obj.author_id}</span>
                    </li>
                );
            } else {
                str = <li>请查询。。。</li>;
            }

        }
        return (
            <div>
                <button onClick={this.getNodeList.bind(this, 'ask')}>查询</button>
                <ul>{str}</ul>
                <button onClick={this.getNodeList.bind(this, 'ask')}>1</button>
                <button onClick={this.getNodeList.bind(this, 'share')}>2</button>
                <button onClick={this.getNodeList.bind(this, 'job')}>3</button>
                <button onClick={this.getNodeList.bind(this, 'good')}>4</button>
            </div>
        )
    }
    getNodeList(tab) {
        const { getNodeList } = this.props;
        getNodeList(tab);
    }
}

// CnodeListWrapper实现的代码在下面
class App extends React.Component {
    render() {
        return (
            <div>
                <CnodeListWrapper></CnodeListWrapper>
            </div>
        )
    }
}

ReactDOM.render(
    <App></App>,
    document.getElementById('root')
)

如果单纯的使用redux方式来进行异步处理,CnodeListWrapper实现代码如下:

class CnodeListWrapper extends React.Component {
    constructor(props) {
        super(props);
        this.state = store.getState();
        this.getNodeList = this.getNodeList.bind(this);
        this.unSubscribeHandle = null;
    }
    getNodeList(tab) {
        store.dispatch(beginFectch());
        // 假设tab为"job"时触发错误
        if (tab === 'job') {
            store.dispatch(fectchFail());
            return;
        }
        $.ajax({
            url: `https://cnodejs.org/api/v1/topics?tab=${tab}&limit=20`,
            success: function (result) {
                if (result.success && result.data) {
                    store.dispatch(fectchSuccess(result.data));
                } else {
                    store.dispatch(fectchFail());
                }
            },
            error: function (jqXHR, textStatus, errorThrown) {
                store.dispatch(fectchFail());
            }
        });
    }
    componentDidMount() {
        this.unSubscribeHandle = store.subscribe(() => {
            this.setState(store.getState())
        });
    }
    componentWillUnmount() {
        this.unSubscribeHandle();
    }
    render() {
        return (
            <CnodeList getNodeList={this.getNodeList} cnode={this.state.cnode}></CnodeList>
        )
    }
}

如上CnodeListWrapper的实现方式增加了数据与组件之间的耦合度不利于后期维护,假如又存在一个组件所依赖的数据源是与CnodeListWrapper相同,上述异步获取数据的代码只能重复的书写一份。

基于上述问题可以我们采用一个中间件重构redux的dispatch方法,重构的方式有很多目前流行的有redux-thunk、redux-promise、redux-saga等。

redux-thunk

上述异步获取数据的代码可以提取成为一个函数,由action creator返回一个函数,该函数作为参数传入store.dispatch,需要注意的是redux的dispatch参数只接受对象,而不是函数,redux-thunk目的就是对store.dispatch进行重构,使其支持函数作为参数。源码如下:

/img/redux-async/redux-thunk.PNG

由上述代码可以看出redux-thunk无非是做一个是否为函数的判断,如果是函数将dispatch、getState等作为参数传入到函数内部,举例代码可以修改如下(有些带忽略):

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk'
import logger from 'redux-logger'
import $ from 'jquery'

// ....

let createStoreWithMiddleware = applyMiddleware(thunk, logger)(createStore);
let store = createStoreWithMiddleware(reducer);

// ....

// action creator
const fetchNodeList = ({ tab }) => {
    return (dispatch, getState) => {
        dispatch(beginFectch());
        $.ajax({
            url: `https://cnodejs.org/api/v1/topics?tab=${tab}&limit=20`,
            success: function (result) {
                if (result.success && result.data) {
                    dispatch(fectchSuccess(result.data));
                } else {
                    dispatch(fectchFail());
                }
            },
            error: function (jqXHR, textStatus, errorThrown) {
                dispatch(fectchFail());
            }
        });
    }
}

// ....

// CnodeListWrapper
class CnodeListWrapper extends React.Component {
    constructor(props) {
        super(props);
        this.state = store.getState();
        this.getNodeList = this.getNodeList.bind(this);
        this.unSubscribeHandle = null;
    }
    getNodeList(tab) {
        store.dispatch(fetchNodeList({tab}));
    }
    componentDidMount() {
        this.unSubscribeHandle = store.subscribe(() => {
            this.setState(store.getState())
        });
    }
    componentWillUnmount() {
        this.unSubscribeHandle();
    }
    render() {
        return (
            <CnodeList getNodeList={this.getNodeList} cnode={this.state.cnode}></CnodeList>
        )
    }
}

这样获取列表数据的任务就交由action creator负责,这样的修改减少了组件的耦合性。往往我们都会将react-redux集成进来,方便获取redux的state代码如下:

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, combineReducers, applyMiddleware } from 'redux';
import { connect, Provider } from 'react-redux'
import thunk from 'redux-thunk'
import logger from 'redux-logger'
import $ from 'jquery'

// ....

const mapStateToProps = (state) => {
    return {
        cnode: state.cnode
    }
}
const mapDispatchToProps = dispatch => {
    return {
        getNodeList: (tab) => {
            dispatch(fetchNodeList({ tab }))
        }
    }
}
const CnodeListWrapper = connect(mapStateToProps, mapDispatchToProps)(CnodeList);

// ....

ReactDOM.render(   
    <Provider store={store}>
        <App></App>
    </Provider>,
    document.getElementById('root')
)

通过使用redux-thunk中间件修改store.dispatch方法,使其能接受函数作为参数,但使用redux-thunk最大的问题,就是重复的模板代码(action creator、reducer的重复戴代码)太多。那么也可以通过其他中间件将其修改为接受其他类型的参数。下面讲述redux-promise

redux-promise

redux-promise这个中间件就是将store.dispatch方法可以接受promise对象,redux-promise源码如下:

/img/redux-async/redux-promise.PNG

从上述代码中可以发现 “isFSA”方法是用来判断它是否是标准的flux action格式。 isFSA实现的源码如下:

FSA格式参考链接

/img/redux-async/isFSA.PNG

可以用两种方式使用redux-promise:1.action直接为promise。 2.action对象的payload属性为promise。使用方式如下:

方式一

// constants
const FETCH_CNODE_LIST = 'FETCH_CNODE_LIST';

// 方式1 action creator
const fectcList = (data) => {
    return {
        type: FETCH_CNODE_LIST,
        payload: data
    }
}
const fetchNodeList = ({ tab }) => new Promise(function (resolve, reject) {
    return $.get(`https://cnodejs.org/api/v1/topics?tab=${tab}&limit=20`).then(result => {
        resolve(fectcList(result.data));
    });
});

有一点需要注意的是redux-promise会判断当前的action是否满足FSA格式,如果不满足,走方式一的路线(action直接为promise对象),但可以发现方式一只能执行resolve后结果,无法执行reject的结果。

方式二

// constants
const FETCH_CNODE_LIST = 'FETCH_CNODE_LIST';

// action creator
const fetchNodeList = createAction(FETCH_CNODE_LIST, ({tab}) => {
    return new Promise(function (resolve, reject) {
        $.get(`https://cnodejs.org/api/v1/topics?tab=${tab}&limit=20`).then(function (result) {
            resolve(result.data);
        }, function () {
            reject({ error: true })
        });
}) });

未完待续。。。