Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

关于Redux的一些总结(一):Action & 中间件 & 异步 #35

Open
dwqs opened this issue Aug 24, 2016 · 8 comments
Open

关于Redux的一些总结(一):Action & 中间件 & 异步 #35

dwqs opened this issue Aug 24, 2016 · 8 comments

Comments

@dwqs
Copy link
Owner

dwqs commented Aug 24, 2016

浅说Flux开发中,简单介绍了Flux及其开发方式。Flux可以说是一个框架,其有本身的 Dispatcher 接口供开发者;也可以说是一种数据流单向控制的架构设计,围绕单向数据流的核心,其定义了一套行为规范,如下图:

flux

Redux的设计就继承了Flux的架构,并将其完善,提供了多个API供开发者调用。借着react-redux,可以很好的与React结合,开发组件化程度极高的现代Web应用。本文是笔者近半年使用react+redux组合的一些总结,不当之处,敬请谅解。

Action

Action是数据从应用传递到 store/state 的载体,也是开启一次完成数据流的开始。

以添加一个todo的Action为例:

{
    type:'add_todo',
    data:'我要去跑步'
}

这样就定义了一个添加一条todo的Action,然后就能通过某个行为去触发这个Action,由这个Action携带的数据(data)去更新store(state/reducer):

store.dispatch({
    type:'add_todo',
    data:'your data'
})

type 是一个常量,Action必备一个字段,用于标识该Action的类型。在项目初期,这样定义Action也能愉快的撸码,但是随着项目的复杂度增加,这种方式会让代码显得冗余,因为如果有多个行为触发同一个Action,则这个Action要写多次;同时,也会造成代码结构不清晰。因而,得更改创建Action的方式:

const ADD_TODO = 'add_todo';

let addTodo = (data='default data') => {
    return {
        type: ADD_TODO,
        data: data
    }
}

//触发action
store.dispatch(addTodo());

更改之后,代码清晰多了,如果有多个行为触发同一个Action,只要调用一下函数 addTodo 就行,并将Action要携带的数据传递给该函数。类似 addTodo 这样的函数,称之为 Action Creator。Action Creator 的唯一功能就是返回一个Action供 dispatch 进行调用。

但是,这样的Action Creator 返回的Action 并不是一个标准的Action。在Flux的架构中,一个Action要符合 FSA(Flux Standard Action) 规范,需要满足如下条件:

  • 是一个纯文本对象
  • 只具备 typepayloaderrormeta 中的一个或者多个属性。type 字段不可缺省,其它字段可缺省
  • 若 Action 报错,error 字段不可缺省,切必须为 true

payload 是一个对象,用作Action携带数据的载体。所以,上述的写法可以更改为:

let addTodo = (data='default data') => {
    return {
        type: ADD_TODO,
        payload: {
            data
        }
    }
}

在 redux 全家桶中,可以利用 redux-actions 来创建符合 FSA 规范的Action:

import {creatAction} from 'redux-actions';

let addTodo = creatAction(ADD_TODO)
//same as
let addTodo = creatAction(ADD_TODO,data=>data)

可以采用如下一个简单的方式检验一个Action是否符合FSA标准:

let isFSA = Object.keys(action).every((item)=>{
   return  ['payload','type','error','meta'].indexOf(item) >  -1
})

中间件

在我看来,Redux提高了两个非常重要的功能,一是 Reducer 拆分,二是中间件。Reducer 拆分可以使组件获取其最小属性(state),而不需要整个Store。中间件则可以在 Action Creator 返回最终可供 dispatch 调用的 action 之前处理各种事情,如异步API调用、日志记录等,是扩展 Redux 功能的一种推荐方式。

Redux 提供了 applyMiddleware(...middlewares) 来将中间件应用到 createStore。applyMiddleware 会返回一个函数,该函数接收原来的 createStore 作为参数,返回一个应用了 middlewares 的增强后的 createStore。

export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => {
    //接收createStore参数
    var store = createStore(reducer, preloadedState, 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)

    //返回经middlewares增强后的createStore
    return {
      ...store,
      dispatch
    }
  }
}

创建 store 的方式也会因是否使用中间件而略有区别。未应用中间价之前,创建 store 的方式如下:

import {createStore} from 'redux';
import reducers from './reducers/index';

export let store = createStore(reducers);

应用中间价之后,创建 store 的方式如下:

import {createStore,applyMiddleware} from 'redux';
import reducers from './reducers/index';

let createStoreWithMiddleware = applyMiddleware(...middleware)(createStore);
export let store = createStoreWithMiddleware(reducers);

那么怎么自定义一个中间件呢?

根据 redux 文档,中间件的签名如下:

({ getState, dispatch }) => next => action

根据上文的 applyMiddleware 源码,每个中间件接收 getState & dispatch 作为参数,并返回一个函数,该函数会被传入下一个中间件的 dispatch 方法,并返回一个接收 action 的新函数。

以一个打印 dispatch action 前后的 state 为例,创建一个中间件示例:

export default function({getState,dispatch}) {
    return (next) => (action) => {
        console.log('pre state', getState());
        // 调用 middleware 链中下一个 middleware 的 dispatch。
        next(action);
        console.log('after dispatch', getState());
    }
}

在创建 store 的文件中调用该中间件:

import {createStore,applyMiddleware} from 'redux';

import reducers from './reducers/index';
import log from '../lib/log';

//export let store = createStore(reducers);

//应用中间件log
let createStoreWithLog = applyMiddleware(log)(createStore);
export let store = createStoreWithLog(reducers);

可以在控制台看到输出:

img

可以对 store 应用多个中间件:

import log from '../lib/log';
import log2 from '../lib/log2';

let createStoreWithLog = applyMiddleware(log,log2)(createStore);
export let store = createStoreWithLog(reducers);

log2 也是一个简单的输出:

export default function({getState,dispatch}) {
    return (next) => (action) => {
        console.log('我是第二个中间件1');
        next(action);
        console.log('我是第二个中间件2');
    }
}

看控制台的输出:

img2

应用多个中间件时,中间件调用链中任何一个缺少 next(action) 的调用,都会导致 action 执行失败

异步

Redux 本身不处理异步行为,需要依赖中间件。结合 redux-actions 使用,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 creator 需要返回一个函数给 redux-thunk 进行调用,示例如下:

export let addTodoWithThunk = (val) => async (dispatch, getState)=>{
    //请求之前的一些处理

    let value = await Promise.resolve(val + ' thunk');
    dispatch({
        type:CONSTANT.ADD_TO_DO_THUNK,
        payload:{
            value
        }
    });
};

效果如下:

thunk

这里之所以不用 createAction,如前文所说,因为 createAction 会返回一个 FSA 规范的 action,该 action 会是一个对象,而不是一个 function:

{
    type: "add_to_do_thunk",
    payload: function(){}
}

如果要使用 createAction,则要自定义一个异步中间件。

export let addTodoWithCustom = createAction(CONSTANT.ADD_TO_DO_CUSTOM, (val) => async (dispatch, getState)=>{
    let value = await Promise.resolve(val + ' custom');
    return {
        value
    };
});

在经过中间件处理时,先判断 action.payload 是否是一个函数,是则执行函数,否则交给 next 处理:

if(typeof action.payload === 'function'){
    let res = action.payload(dispatch, getState);
} else {
    next(action);
}

而 async 函数返回一个 Promise,因而需要作进一步处理:

res.then(
    (result) => {
        dispatch({...action, payload: result});
    },
    (error) => {
        dispatch({...action, payload: error, error: true});
    }
);

这样就自定义了一个异步中间件,效果如下:

custom

当然,我们可以对函数执行后的结果是否是Promise作一个判断:

function isPromise (val) {
    return val && typeof val.then === 'function';
}

//对执行结果是否是Promise
if (isPromise(res)){
    //处理
} else {
    dispatch({...action, payload: res});
}

那么,怎么利用 redux-promise 呢?redux-promise 是能处理符合 FSA 规范的 action 的,其对异步处理的关键源码如下:

action.payload.then(
    result => dispatch({ ...action, payload: result }),
    error => {
        dispatch({ ...action, payload: error, error: true });
        return Promise.reject(error);
    }
)

因而,返回的 payload 不再是一个函数,而是一个 Promise。而 async 函数执行后就是返回一个 Promise,所以,让上文定义的 async 函数自执行一次就可以:

export let addTodoWithPromise = createAction(CONSTANT.ADD_TO_DO_PROMISE, (val) =>
    (async (dispatch, getState)=>{
        let value = await Promise.resolve(val + ' promise');
        return {
            value
        };
    })()
);

结果如下图:

promise

示例源码:redux-demo

@dwqs dwqs changed the title 关于Redux的一些总结(一) 关于Redux的一些总结(一):Action & 中间价 & 异步 Aug 24, 2016
@dwqs dwqs changed the title 关于Redux的一些总结(一):Action & 中间价 & 异步 关于Redux的一些总结(一):Action & 中间件 & 异步 Aug 24, 2016
@fengsx
Copy link

fengsx commented Sep 28, 2016

图挂了

@linxizhilu
Copy link

领教了大神...顺便mark走了

@DennisLi87
Copy link

写的不错

@lcoder
Copy link

lcoder commented Mar 31, 2017

后面的异步没看懂😂,恩,19年的我懂了

@likaihua
Copy link

学习了

@dwqs
Copy link
Owner Author

dwqs commented Feb 27, 2018

@zhongdeming428 多谢指出 已纠正

@lovelifeloveyou
Copy link

向大佬学习

@yzfdjzwl
Copy link

yzfdjzwl commented Jan 1, 2019

m

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants