看过react的人都知道, react是一个view层的展现库,要想实现对页面数据和路由的管理还需要配合其它的库。这其中最常用的就是redux和react-router库。
通过redux库能够统一管理页面数据,保证数据的单向流动,其大概流程是 用户触发页面交互,页面根据用户交互产生一个action并将这个action通过store的dispatch方法传给
sotre,store根据一定关系找到reducer,reducer根据action的type类型产生新的state值并将新产生的state值回传给store,store根据最新的state通知view重新渲染页面(通过调用render函数)。而react-rouer则用来控制react中路由跳转。这样通过react redux react-router相互配合形成类似于mvc的前端页面结构。
在上面的流程中store是数据管理的源头,但是store的数据又来自哪里呢?可以说绝大多数数据来自服务端,这里又可以分为两类数据,一类是在页面初始化的时候由服务端直接提供的数据,另一类是通过用户交互从服务端获取的数据。对于第二类数据主要是通过异步方式(ajax或者promise或者生成器或者await/async)获取的数据。今天主要记录一下自己对react和redux配合使用时如何将异步获取数据流程融合进去的理解。
在用户操作页面得到结果的过程中主要经历了以下几个主要过程 1.用户交互产生action 2.dispatch分发action 3.reducer产生新的state值 4.storoe通知view重新渲染页面。如果要把异步获取数据的操作融合进去只有1 2两个阶段,而这两个阶段中只有在产生action之后 分发action之前是最合适的时机。要想了解这其中的运行机制,就必须了解react中间件的原理。中间件很好理解,就是一个处理过程,有输入有输出,但是中间加入了一些其它操作,类似于设计模式中的装饰模式。经过中间件的处理后,能够增加一些其它功能,比如日志记录功能,数据上报功能等等。
我们在学习react的时候会看到很多关于如何 使用react和redux的示例代码,在创建store的时候有些是这样的:
const store=createStore1(rootReducer,initialState);
也有些是这样:
createStore1 = applyMiddleware(thunk)(createStore)(rootReducer,initialState);
第一种方式是中规中矩的示例写法,创建了一个具有最基本功能的store对象。第二种方式为store加入了其它功能,加入这个功能使用的就是redux的中间件函数 applyMiddleware,这也是react支持高扩展性的关键,可以通过applyMiddleware函数为react应用添加多种功能,而它的实现方式主要是通过高阶组件实现的。我们可能头一次听说高阶组件这个词汇,但是高阶函数相信大多人都听说过,也写过,高阶函数中是把一个函数作为参数传入,经过修饰产生一个新的函数,同理如果传入是react组件输出的也是经过修饰的新组件那么就可以称为高阶组件(个人理解),类似于装饰模式。接下来让我们一起分析下这个applylMiddleware是个什么东西,怎么就为react应用添加了其它功能。
先贴出applyMiddleware的实现代码:
1 export default function applyMiddleware(...middlewares) { 2 return (createStore) => (reducer, preloadedState, enhancer) => { 3 const store = createStore(reducer, preloadedState, enhancer) 4 let dispatch = store.dispatch 5 let chain = [] 6 7 const middlewareAPI = { 8 getState: store.getState, 9 dispatch: (action) => dispatch(action)10 }11 chain = middlewares.map(middleware => middleware(middlewareAPI))12 dispatch = compose(...chain)(store.dispatch)13 14 return {15 ...store,16 dispatch17 }18 }19 }
上面代码中是es6写法,export用于导出函数 类似于module.exports的作用,在另一个文件中用import进行导入。参数的...middleware是扩展运算符可以把数组转成对应的参数。=>是箭头函数。对于export ... 都好理解,对于return加上两个箭头函数的组合,可能还不是太适应,下面把这部分转成我们熟悉的形式:
export default function applyMiddleware(...middlewares) { return function(createStore) { return funciton(reducer, preloadedState, enhancer) { const store = createStore(reducer, preloadedState, enhancer) let dispatch = store.dispatch let chain = [] const middlewareAPI = { getState: store.getState, dispatch: (action) = > dispatch(action) } chain = middlewares.map(middleware = > middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch } } } }
通过改成我们熟悉的格式可以很直观的发现所谓的中间件就是传入一些参数 然后返回了一个函数(第一个函数fun1),而返回的这个函数再次传入一些参数又返回了一个新的函数(第二个函数 fun2),fun2再次传入一些参数就会执行最内层的函数体。对照 applyMiddleware(thunk)(createStore)(rootReducer,initialState);我们可以做相应的替换其中applyMiddleware(thunk)可以替换成fun1,替换后的结果是是fun1(createStore)(rootReducer,initialState) 然后把fun1(createStore)替换成fun2 其结果fun2(rootReducer,initialState) 这样我们逐层分解就能得到我们最熟悉最简单的函数调用形式。通过这种高阶函数的调用在内部可以保持对外层传入参数的引用,在需要使用的时候调用,可以理解为对外层传入参数的延迟使用。接下来我们看一下经过三层参数的传入在最内层到底做了些什么事情。在此提醒一下,不要被参数名误导,它只是一个参数,不具体指任何对象。applyMiddlleware的函数只有这么几行,让我们一行一行的分析。
还是从applyMiddleware(thunk)(createStore)(rootReducer,initialState);这个代码说起,第一层数传入的是thunk,只有一个,为了和applyMiddleware的源码参数保持一致,我们假定thunk是一个数组,第二 层参数是createStore 是redux的核心函数之一,用于创建store,第三层参数是rootReducer,initialState 对应applyMiddleware源码中的reducer, preloadedState, enhancer,只不过一个传入两个参数一个三个参数,这些都不重要,目前我们只使用第一个参数。
函数体内第一行代码 const store = createStore(reducer, preloadedState, enhancer) 用传入的第二层参数createStore(此参数是一个函数)和第三层参数创建一个store对象,用真是参数代替就是 const store = createStore(rootReducer,initialState) 。
第二句代码 let dispatch = store.dispatch;保存store的dispatch函数的引用。
第三句代码 let chain = [] 建立一个空数组对象。
第四句代码
const middlewareAPI = { getState: store.getState, dispatch: (action) = > dispatch(action) }
定义一个对象,对象有两个属性,都是函数 一个sotre的getState函数用于返回store,一个dispatch函数。
第五句代码 chain = middlewares.map(middleware = > middleware(middlewareAPI)) 遍历第一层参数传入的数组,数组中是用到的各种中间件函数,中间件函数接受第四句代码定义的middlewareAPI对象作为入参,最终返回一个有一系列中间件函数返回的结果组成的数组对象。
第六句代码 dispatch = compose(...chain)(store.dispatch) 将第五句代码产生的chain作为第一层参数 store.dispatch作为第二层参数,这里的第一层参数和第二层参数要和上面的第一层 第二层 区分开,本文中提到的第几层参数都指上面的提到的参数。compose是一个组合函数经过compose的高阶函数调用返回的结果赋值给第二句代码中的dispatch对象,注意经过 2 3 4 5 步后 dispatch对象已经被重新定义。
第七句代码 return {...store, dispatch} 返回新的store对象,其中dispatch会覆盖store中的dispatch对象。至此一共七句代码执行完毕 我们最终得到一个增强的store对象。那中间件函数是如何发挥作用的呢,奥秘就在第五句和第六句代码中,让我们继续解析。
在解析之前先让我们看看 react中间件长什么样子,以下是一个最简单的示例:
export default sotre=>next=>action=>{ console.log('someInfo'); next(action); consoe.log('someInfoOther');}
react中中间件也是以高阶组件的形式出现,并使用es6语法,如果 看着不服输可以按照上面的方式将es6改成我们熟悉的es5形式。
export default function (sotre){ return function(next){ //fun1 return function(action){ //fun2 console.log('someInfo'); next(action); consoe.log('someInfoOther'); } }}
好了我们继续回到applyMiddleware函数的第五句代码 chain = middlewares.map(middleware = > middleware(middlewareAPI)) 中间件的数组执行以后返回一个新数组,按照上面中间件的形式套进去 我们会得到一个 fun1函数的数组我们记为fun1Arr([f1,f2,.....fn])。我们再来看applyMiddleware的第六句代码 dispatch = compose(...chain)(store.dispatch) ,将上一步返回的函数数组fun1Arr传入函数compose(理解这个函数很重要)中,我们继续看compose的实现
export default function compose(...funcs) { if (funcs.length === 0) { return arg => arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) => (...args) => a(b(...args)))}
这个函数也很简单前两个if 可以直接忽略,最后一句的reduce函数也很简单,就是数组的reduce函数,简单介绍一下,语法格式为:
arr.reduce([callback, initialValue]) callback是执行数组中每个值的函数,包括四个参数 1.previousValue上一次调用回调函数返回的值,或者是提供的初始值(initialValue) 2.currentValue数组中当前被处理的元素 3.currentIndex当前被处理元素在数组中的索引, 即currentValue的索引.如果有initialValue初始值, 从0开始.如果没有从1开始 4.arr 当前调用的数组。
initialValue是传入的初始值。这个函数看着很简单,但当把fun1Arr传入就不简单了。注意reduce里是有两个箭头函数的函数。为了便于理解 我们改成es5形式
return funcs.reduce(function(a,b){ return function(...args){ //fr_x_y 函数 a(b(...args)); }})
对应reduce的原型函数可以发现 只有一个callback参数,没有initialValue参数,对于没有initialValue值得reduce函数 回调函数第一次执行时,previousValue
和 currentValue
可能是两个不同值其中的一个,如果reduce有
initialValue参数
,那么 previousValue
等于 initialValue
,并且currentValue
等于数组中的第一个值;如果reduce
没有 initialValue
参数,那么previousValue
等于数组中的第一个值,currentValue
等于数组中的第二个值。对于fun1Arr传入compose函数后得到一个层层调用的函数,其形式为:
f1(f2(f3(fn(...args)))),注:fun1Arr的形式为[f1,f2,f3,....fn]。至此我们得到了compose执行后最终的函数 形式f1(f2(f3(fn(...args)))),其中fn表示applyMiddleware中传入的中间件函数。再回过头来看applyMiddleware的第6句代码dispatch = compose(...chain)(store.dispatch) ,其中compose(...chain)返回的就是f1(f2(f3(fn(...args))))这个函数,最后传入store.dispatch参数将返回的结果又赋值给dispatch,这里最终是对dispatch进行了一次改写,换句话说就是通过高阶函数增强了dispatch,让原本单调的dispatch函数变得丰富起来。applyMiddleware函数的最后将增强后的dispath函数重新赋值给store返回。接下来我们结合中间件的实现方式 将f1(f2(f3(fn(store.dispatch)))) 【将...args替换成store.dispatch】函数展开。上面我们已经有了中间件函数的形式(高阶函数)并且在applyMiddleware中第5句代码处执行过一次中间件函数,所以这里的fn对应的函数格式就是
return function(next){ //fun1 return function(action){ //fun2 console.log('someInfo'); next(action); consoe.log('someInfoOther'); } }
将上面的fun1函数带入f1(f2(f3(fn(store.dispatch)))) 函数后层层展开 我们最终得到一个新函数,其格式为
function(action){ 中间件1执行的操作 中间件2执行的操作 ........ 中间件n执行的操作 store.dispatch(action);}
所以store中dispatch被增强后的形式就是上面的形式,在view中调用dispath(action)的时候就是上面函数执行的过程。至此我们把react中间件的流程梳理了一遍。在文章的开头我们讲到如何将react redux 异步请求数据结合到一起,现在我们理解了中间件的过程,再说异步请求数据是如何整合到redux中就简单许多了。还是先看一下异步中间件的源码我们以react-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做了一个判断如果action是function 那就执行这个函数,并把dispatch 和 getState传入函数。这里需要注意一点如果action是函数就会自行此函数 不会next(action)了 所以异步中间件要放在所有中间件的最后,面得其它功能型中间件不起作用。
好了,终于把react中间件的主要流程梳理完了,最后再补充一句 高阶函数是层层调用的 每层调用时传入的参数都会随着层数的不同而不同。
。