资讯专栏INFORMATION COLUMN

Redux:Middleware你咋就这么难

superPershing / 2895人阅读

摘要:接下来的函数就有点难度了,让我们一行一行来看。上面实际的含义就是将数组每一个执行的返回值保存的数组中。需要注意的是,方法返回值并不是数组,而是形如初始值的经过叠加处理后的操作。从而实现异步的。

  这段时间都在学习Redux,感觉对我来说初学难度很大,中文官方文档读了好多遍才大概有点入门的感觉,小小地总结一下,首先可以看一下Redux的基本流程:

  从上面的图可以看出,简单来说,单一的state是存储在store中,当要对state进行更新的时候,首先要发起一个action(通过dispatch函数),action的作用就是相当于一个消息通知,用来描述发生了什么(比如:增加一个Todo),然后reducer会根据action来进行对state更新,这样就可以根据新的state去渲染View。
  
  当然上面仅仅是发生同步Action的情况下,如果是Action是异步的(例如从服务器获取数据),那么情况就有所不同了,必须要借助Redux的中间件Middleware。
  

Redux moddleware provides a third-party extension point between dispatching an action, and the moment it reaches the reducer

  根据官方的解释,Redux中间件在发起一个actionaction到达reducer的之间,提供了一个第三方的扩展。本质上通过插件的形式,将原本的action->redux的流程改变为action->middleware1->middleware2-> ... ->reducer,通过改变数据流,从而实现例如异步Action、日志输入的功能。
  首先我们举例不使用中间件Middleware创建store:

import rootReducer from "./reducers"
import {createStore} from "redux"

let store =  createStore(rootReducer);

  那么使用中间件的情况下(以redux-logger举例),创建过程如下:

import rootReducer from "./reducers"
import {createStore,applyMiddleware} from "redux"
import createLogger from "redux-logger"

const loggerMiddleware = createLogger();
let store = applyMiddleware(loggerMiddleware)(createStore)(rootReducer);

  
  那么我们不经要问了,为什么采用了上面的代码就可以实现打印日志的中间件呢?
  首先给出applyMiddleware的源码(Redux1.0.1版本):

export default function applyMiddleware(...middlewares) {            return (next)  => 
        (reducer, initialState) => {

              var store = next(reducer, initialState);
              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);
              return {
                ...store,
                dispatch
              };
           };
}

  上面的代码虽然只有不到20行,但看懂确实是不太容易,实际上包含了函数式编程各种技术,首先最明显的使用到了柯里化(Currying),在我理解中柯里化(Currying)实际就是将多参数函数转化为单参数函数并延迟执行函数,例如:

function add(x){
    return function(y){
        return x + y;
    }
}
var add5 = add(5);
console.log(add5(10)); // 10

  关于柯里化(Currying)更详细的介绍可以看我之前的一篇文章从一道面试题谈谈函数柯里化(Currying)。
  首先我们看applyMiddleware的总体结构:

export default function applyMiddleware(...middlewares) {            return (next)  => 
        (reducer, initialState) => {
        };
}

  哈哈,典型的柯里化(Currying),其中(...middlewares)用到了ES6中的新特性,用于将任意个中间件参数转化为中间件数组,因此很容易看出来在该函数的调用方法就是:

let store = applyMiddleware(middleware1,middleware2)(createStore)(rootReducer);

  其中applyMiddleware形参和实参的对应关系是:

形参 实参
middlewares [middleware1,middleware2]
createStore Redux原生createStore
reducer, preloadedState, enhancer 原生createStore需要填入的参数

  再看函数体:

var store = next(reducer, initialState);
var dispatch = store.dispatch;
var chain = [];
var middlewareAPI = {
    getState: store.getState,
    dispatch: (action) => dispatch(action)
};

  上面代码非常简单,首先得到store,并将之前的store.dispatch存储在变量dispatch中,声明chain,以及将middleware需要的参数存储到变量middlewareAPI中。接下来的函数就有点难度了,让我们一行一行来看。

chain = middlewares.map(middleware => middleware(middlewareAPI))

  上面实际的含义就是将middleware数组每一个middleware执行
middleware(middlewareAPI)的返回值保存的chain数组中。那么我们不经要问了,中间件函数到底是怎样的?我们提供一个精简版的createLogger函数:

export default function createLogger({ getState }) {
      return (next) => 
        (action) => {
              const console = window.console;
              const prevState = getState();
              const returnValue = next(action);
              const nextState = getState();
              const actionType = String(action.type);
              const message = `action ${actionType}`;

              console.log(`%c prev state`, `color: #9E9E9E`, prevState);
              console.log(`%c action`, `color: #03A9F4`, action);
              console.log(`%c next state`, `color: #4CAF50`, nextState);
              return returnValue;
    };
}

  可见中间件createLogger也是典型的柯里化(Currying)函数。{getState}使用了ES6的解构赋值,createLogger(middlewareAPI))返回的(也就是数组chain存储的是)函数的结构是:

(next) => (action) => {
//包含getState、dispatch函数的闭包
};

  我们接着看我们的applyMiddleware函数

dispatch = compose(...chain,store.dispatch)

  这句是最精妙也是最有难度的地方,注意一下,这里的...操作符是数组展开,下面我们先给出Redux中复合函数compose函数的实现(Redux1.0.1版本):

export default function compose(...funcs) {
     return funcs.reduceRight((composed, f) => f(composed));
}

  首先先明确一下reduceRight(我用过的次数区区可数,所以介绍一下reducereduceRight)
  

Array.prototype.reduce.reduce(callback, [initialValue])

reduce方法有两个参数,第一个参数是一个callback,用于针对数组项的操作;第二个参数则是传入的初始值,这个初始值用于单个数组项的操作。需要注意的是,reduce方法返回值并不是数组,而是形如初始值的经过叠加处理后的操作。
callback分别有四个参数:

accumulator:上一次callback返回的累积值

currentValue: 当前值

currentIndex: 当前值索引

array: 数组
例如:

var sum = [0, 1, 2, 3].reduce(function(a, b) {
return a + b;
}, 0);
// sum is 6

  reducereduceRight的区别就是从左到右和从右到左的区别。所以如果我们调用compose([func1,func2],store.dispatch)的话,实际返回的函数是:

//也就是当前dispatch的值
func1(func2(store.dispatch))

  胜利在望,看最后一句:

return {
    ...store,
    dispatch
};

  这里其实是ES7的用法,相当于ES6中的:

return Object.assign({},store,{dispatch:dispatch});

  或者是Underscore.js中的:

return _.extends({}, store, { dispatch: dispatch });

  懂了吧,就是新创建的一个对象,将store中的所有可枚举属性复制进去(浅复制),并用当前的dispatch覆盖store中的dispatch属性。所以

let store = applyMiddleware(loggerMiddleware)(createStore)(rootReducer);

中的store中的dispatch属性已经不是之前的Redux原生的dispatch而是类似于func1(func2(store.dispatch))这种形式的函数了,但是我们不禁又要问了,那么中间件Miffffdleware又是怎么做的呢,我们看一下之前我们提供的建议的打印日志的函数:

export default function createLogger({ getState }) {
      return (next) => 
        (action) => {
              const console = window.console;
              const prevState = getState();
              const returnValue = next(action);
              const nextState = getState();
              const actionType = String(action.type);
              const message = `action ${actionType}`;

              console.log(`%c prev state`, `color: #9E9E9E`, prevState);
              console.log(`%c action`, `color: #03A9F4`, action);
              console.log(`%c next state`, `color: #4CAF50`, nextState);
              return returnValue;
    };
}

  假设一下,我们现在使用两个中间件,createLoggercreateMiddleware,其中createMiddleware的函数为

export default function createMiddleware({ getState }) {
      return (next) => 
        (action) => {
        return next(action)
    };
}

调用形式为:

let store = applyMiddleware(createLogger,createMiddleware)(createStore)(rootReducer);

如果调用了store.dispatch(action),chain中的两个函数分别是
createLoggercreateMiddleware中的

(next) => (action) => {}

部分。我们姑且命名一下chain中关于createLogger的函数叫做
func1,关于createMiddleware的函数叫做func2。那么现在调用
store.dispatch(action),实际就调用了(注意顺序)

//这里的store.dispatch是原始Redux提供的dispatch函数
func1(func2(store.dispatch))(action)

  上面的函数大家注意之前执行次序,首先func2(store.dispatch再是func1(args)(action)。对于func1获得的next的实参是参数是:

(action)=>{
    //func2中的next是store.dispatch
    next(action);
}

  那么实际上func1(...)(action)执行的时候,也就是

const console = window.console;
const prevState = getState();
const returnValue = next(action);
const nextState = getState();
const actionType = String(action.type);
const message = `action ${actionType}`;

console.log(`%c prev state`, `color: #9E9E9E`, prevState);
console.log(`%c action`, `color: #03A9F4`, action);
console.log(`%c next state`, `color: #4CAF50`, nextState);
return returnValue;

的时候,getState调用的闭包MiddlewareAPI中的Redux的getState函数,调用next(action)的时候,会回调createMiddleware函数,然后createMiddlewarenext函数会回调真正的store.dispatch(action)。因此我们可以看出来实际的调用顺序是和传入中间件顺序相反的,例如:

let store = applyMiddleware(Middleware1,Middleware2,Middleware3)(createStore)(rootReducer);

实际的执行是次序是store.dispatch->Middleware3->Middleware2->Middleware1
  不知道大家有没有注意到一点,

var middlewareAPI = {
    getState: store.getState,
    dispatch: (action) => dispatch(action)
};

并没有直接使用dispatch:dispatch,而是使用了dispatch:(action) => dispatch(action),其目的是如果使用了dispatch:dispatch,那么在所有的Middleware中实际都引用的同一个dispatch(闭包),如果存在一个中间件修改了dispatch,就会导致后面一下一系列的问题,但是如果使用dispatch:(action) => dispatch(action)就可以避免这个问题。
  接下来我们看看异步的action如何实现,我们先演示一个异步action creater函数:

export const FETCHING_DATA = "FETCHING_DATA"; // 拉取状态
export const RECEIVE_USER_DATA = "RECEIVE_USER_DATA"; //接收到拉取的状态
export function fetchingData(flag) {
    return {
        type: FETCHING_DATA,
        isFetchingData: flag
    };
}

export function receiveUserData(json) {
    return {
        type: RECEIVE_USER_DATA,
        profile: json
    }
}
export function fetchUserInfo(username) {
    return function (dispatch) {
        dispatch(fetchingData(true));
        return fetch(`https://api.github.com/users/${username}`)
            .then(response => {
                console.log(response);
                return response.json();
            })
            .then(json => {
                console.log(json);
                return json;
            })
            .then((json) => {
                dispatch(receiveUserData(json))
            })
            .then(() => dispatch(fetchingData(false)));
    };
}

  上面的代码用来从Github API中拉取名为username的用户信息,可见首先fetchUserInfo函数会dispatch一个表示开始拉取的action,然后使用fetch函数访问Github的API,并返回一个Promise,等到获取到数据的时候,dispatch一个收到数据的action,最后dispatch一个拉取结束的action。因为普通的action都是一个纯JavaScript Object对象,但是异步的Action却返回的是一个function,这是我们就要使用的一个中间件:redux-thunk。
  我们给出一个类似redux-thunk的实现:

export default function thunkMiddleware({ dispatch, getState }) {
      return next => 
             action => 
                   typeof action === ‘function’ ? 
                     action(dispatch, getState) : 
                     next(action);
}

这个和你之前看到的中间件很类似。如果得到的action是个函数,就用dispatch和getState当作参数来调用它,否则就直接分派给store。从而实现异步的Action。
  Redux入门学习,如果有写的不对的地方,希望大家指正,欢迎大家围观我的博客:
  
  MrErHu
  SegmentFault

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/81486.html

相关文章

  • Redux 进阶 - react 全家桶学习笔记(二)

    摘要:在函数式编程中,异步操作修改全局变量等与函数外部环境发生的交互叫做副作用通常认为这些操作是邪恶肮脏的,并且也是导致的源头。 注:这篇是17年1月的文章,搬运自本人 blog... https://github.com/BuptStEve/... 零、前言 在上一篇中介绍了 Redux 的各项基础 api。接着一步一步地介绍如何与 React 进行结合,并从引入过程中遇到的各个痛点引出 ...

    Godtoy 评论0 收藏0
  • redux深入进阶

    摘要:上一篇文章讲解了如何使用,本篇文章将进一步深入,从的源码入手,深入学习的中间件机制。的功能是让支持异步,让我们可以在中跟服务器进行交互等操作,而他的实现。。。 上一篇文章讲解了redux如何使用,本篇文章将进一步深入,从redux的源码入手,深入学习redux的中间件机制。在这里我们会以一个redux-thunk中间件为例,逐步分解redux的中间机制如何操作,如何执行。 闲话不多说,...

    omgdog 评论0 收藏0
  • JavaScript之原型与原型链

    摘要:个人博客原文地址万物皆对象在中除值类型之外,其他的都是对象,为了说明这点,我们举几个例子我们可以使用来做类型判断除了属于值类型之外,其他都是对象。 个人博客原文地址 万物皆对象 在JavaScript中除值类型之外,其他的都是对象,为了说明这点,我们举几个例子我们可以使用typeof来做类型判断 typeof a; // undefined typeof 1; ...

    wuyangchun 评论0 收藏0
  • Redux源码分析

    摘要:在得到新的状态后,依次调用所有的监听器,通知状态的变更。执行完后,获得数组,它保存的对象是第二个箭头函数返回的匿名函数。部分源码利用这个属性,所有子组件均可以拿到这个属性。 Redux使用中的几个点: Redux三大设计原则 Create Store Redux middleware combineReducer Provider与Connect Redux流程梳理 Redux设计特...

    renweihub 评论0 收藏0
  • Redux 中间件分析

    摘要:假设等于,其中,,是三个中间件,等于,那么可以简化为。最终返回中的方法以及经过中间件包装处理过的方法。以此类推,第二个返回的就是第一个中间件的形参。根据这个的讨论,在中间件顶层调用了,结果导致无法执行后面的中间件。 redux 主要包含 5 个方法,分别是: createStore combineReducers bindActionCreators applyMiddleware ...

    littlelightss 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<