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

[2018-08-26]: 阅读Redux源码05 - applyMiddleware和compose #9

Open
Lemonreds opened this issue Aug 26, 2018 · 0 comments
Open

[2018-08-26]: 阅读Redux源码05 - applyMiddleware和compose #9

Lemonreds opened this issue Aug 26, 2018 · 0 comments

Comments

@Lemonreds
Copy link
Owner

Lemonreds commented Aug 26, 2018

首先我们来看看中间件的定义:action发起之后的数据流是这样的--action -> reducer -> store,而加上中间件的处理之后,数据流就变成 action -> middlewareA -> middlewareB ->... -> reducer -> store,相当于在抵达Reucer之前 action 可以进行扩展,也就是说我们可以控制每一个流过的action,选择一些我们期待做出修改的action进行响应操作。常用的中间件有异步支持(redux-thunk)、打印日志(redux-logger)等。

compose 方法

在解读applyMiddleware之前,先看看Redux另一个API,也是applyMiddleware里面使用到的方法,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)))
}

方法接收一个函数数组,当数组长度大于1时,才开始化合作用。最难理解的部分是方法的返回,Array.reduce累加器方法(可参考MDN文档说明)函数的第一个参数是callback回调,表示数组中的每个元素(从左到右)都应用这个函数,compose中累加器的callback回调展开来看也就是:

funcs.reduce(function (a, b) {
    return (...args) => a(b(...args))
})

其中 a 是上一次执行完callback函数的返回值,b是数组当前元素处理的值。

遍历完函数数组的所有元素后,会返回一个强力函数,第一个数组元素包裹在最外层,最后一个数组元素包裹在最里层,这个函数接受任意的参数 ...args,并从数组后面开始向前,逐个执行,执行完之后的返回值作为下一个函数的参数层层传递。

看起来有点绕,看一个例子就明白了:

let final = compose(f,g,h)
// final等价于 函数 f(g(h(...args)))
// 此时使用 final 函数的话
// 如随意传递一个参数
final(666)
// 详细执行过程就是:
// h(666) => 执行函数,获取返回值 A
// g(A) => 将A作为参数传递给g函数执行,获取返回值 B
// f(B) => 将B作为参数传递给f函数执行,获取返回值 C
// ...以此类推

可能我们现在还不知道compose到底有什么用,但没关系,稍后就能看到compose的魔法。

applyMiddleware 预览

方法签名

首先来看applyMiddleware的方法签名:

export default function applyMiddleware(...middlewares) 

参数

  • ...middlewares  函数数组,也就是一个个中间件,不同与其他函数的是,中间件函数需要符合一定规则,才能兼容redux,发挥作用,具体的规则参考下方的例子。

方法返回

applyMiddleware方法返回一个函数,形如:

return createStore => (...args) => {
    //....
    return {
        //.....
    }
}

applyMiddleware 解读

通常,我们会这样使用 applyMiddleware 方法:

const store = createStore(
    reducer,
    applyMiddleware(...middlewares)
)

还记得在方法createStore()中,一旦遇到如上的调用形式,就会直接返回如下形式:

return enhancer(createStore)(reducer, preloadedState)

对这行代码进行拆分,并进行分析:

// 上面我们可以拆成两部分来看
const tmp = enhancer(createStore)  // 1
return tmp (reducer, preloadedState) //2

// 这里的 enhancer 就是 applyMiddleware(...middlewares) 返回的函数:
createStore => (...args) => {
    //xxxx
    return {
        //xxx
    }
}

也就是说,creatStore方法中一旦遇到了应用中间件参数的时候,会依次传入 createStore(自身),reducer和reloadedState,层层执行,最终返回一个对象,而这个对象具体的内容则由applyMiddleware方法具体定义。
那么creatStore遇到中间件的情况到底返回了什么,我们接着看看applyMiddleware的详细代码,直接看到内部返回对象的那一个函数中:

return createStore => (...args) => {
    /**
     * 函数执行到这里的时候 可以使用的参数有:
     * ...middlewares 就是使用 applyMiddleware(...middlewares) 时传入的中间件函数数组
     * createStore = Store.createStore() 方法
     *  ...args == (reducer, preloadedState)
     */
    // 初始化store
    const store = createStore(...args)
    //xxxx  一系列处理
    return {
      ...store,
      dispatch
    }  
  }

我们看到,函数用传入的参数去初始化了一个Store,接着,经过一系列处理之后,返回了这个store,并用一个dispatch覆盖了原来store中的dispatch方法。到这里我们可以发现,creatStore遇到中间件的情况的时候,返回值和createStore原有返回的Store对象相同,提供相同的方法,不同的是dispatch被覆盖了,或者说经过处理之后被增强了。

再接着看函数发生了什么:

    // 这里定义了一个 dispatch 方法 
    // 暂时不用关注其用途
    // 后续会说道原因
    let dispatch = () => {
      throw new Error(
        `xxxx`
      )
    }    


    const middlewareAPI = {
      getState: store.getState,
      // 为 dispatch 包装一层 使得dispatch 支持传入多个参数
      dispatch: (...args) => dispatch(...args)
    }

这里声明了middlewareAPI 常量,作用就是Redux暴露给中间件的接口,一个getState()函数,来自原生Store,和一个dispatch方法,注意这个方法并非Store原生,稍候会解释为什么要这样处理。

再看下一部分之前,先介绍一个用于异步处理的Redux中间件,redux-thunk,代码十分简单,寥寥几行,实际上,为了同Redux兼容,中间件函数的设计标准都大同小异:

export default const thunk = ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {    
       return action(dispatch, getState)
    } 
    return next(action)
}
// 先记住中间件的函数签名和返回,稍候再来详细讲解 reat-thunk 是如何发挥作用的

接着回到 applyMiddleware 的代码中,下一行:

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

结合上面的redux-thunk,这段代码就不难理解,遍历所有传入的中间件函数,传入middlewareAPI执行,将返回值放到chain数组,这段代码执行完后,chain中的中间件函数大概是这样子的:

// middlewareAPI 中的两个参数已经传入
// 由于闭包的存在,函数内部可以直接使用 getState 和 dispatch
next => action => {
    // 中间件处理逻辑.....
    return next(action)
}

接着应用了compose,这一步是最难理解,同时也是最灵巧的一部分:

dispatch = compose(...chain)(store.dispatch)
// 我们把这一步拆成两部分
// 第一步
const final = compose(...chain)
// 第二步
dispatch = final (store.dispatch)

如果我们有a,b,c三个中间件,那么第一步代码执行完成之后,final是一个这样的函数:

const final = a(b(c(...args)))

而且a,b,c 三个函数的定义都是统一的形如:

next => action => {
     // 中间件处理逻辑.....
     return next(action)
 }

再看第二步,将Store原生的 dispatch 方法作为参数传入 final 函数 ,得到的结果赋值给 dispatch 变量:

dispatch = a(b(c(store.dispatch)))

// 为了便于理解 我们来分布执行 a(b(c(store.dispatch)))

// store.dispatch 作为参数 只会传递给第一个函数,这里也就是c
// 其他的函数依次接收上一个函数的返回值作为参数,详细过程:

store.dispatch 作为 next 参数给 c
c( next ) 执行后返回函数 action =>{ return store.dispatch(action)} 作为 next 参数传给b

b( next ) 执行后返回函数 action =>{ return next(action)} 作为 next 参数传给a
a( next ) 执行后返回函数 action =>{ return next(action)} 

当过程结束后,变量dispatch就会得到 a 返回的一个这样的函数:

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

可能到这里我们还是不能明白它是如何执行多个中间件逻辑的,我们来写一个例子:

// 3个中间件 a,b,c
let c = ({getState,dispatch}) => next => aciton =>{
    console.log('c')
    return next(action)
}
let b = ({getState,dispatch}) => next => aciton =>{
    console.log('b')
    return next(action)
}
let a = ({getState,dispatch}) => next => aciton =>{
    console.log('a')
    return next(action)
}
// 我们在代码中这样调用它们
// 注意调用的顺序
const store = createStore(reducer,applyMiddleware(a,b,c))
// 随便触发一个事件
store.dispath({ type: 'HELLO_WORLD '})
// 此时得到的输出是
a
b
c
// 也就是首先执行完a的逻辑,next(action)时 跳到 b处理
// 同样 b 执行到next(action)时 跳到c处理
// c 执行 next(action) ,这个next是Store原生的dispatch,也就是真正发起action的时候
// 最后将原生的dispatch的返回值一一传递,返回即可

还记得我们之前说过的 原生Store.dispatch的返回值是什么吗?不记得可以重新看一下createStore的代码,dispatch的返回值就是action,增强的dispatch方法也同样层层返回了action,即便应用了中间件,入参和返回都是相同的,只是过程不同。

现在,我们就能明白中间件的作用和如何生效的了。中间件更像是一条链子,传入一个action后,每一个中间件都可以对action进行处理,经过一个个中间件处理后的action,最终使用了原生的Store.dispatch()来发起,应用中间件的修改。注意,中间件的next(action)是必须的,如果不调用next,就会使得中间件执行链断开,导致最终不能发起action。

redux-thunk

说明一下,异步操作的时候,我们期待发起多个action,例如,获取数据之前发起一个action,正在获取数据也可以发起一个action,获取到数据的时候再发起一个action。接下来我们来看看redux-thunk是如何实现支持异步操作的,在此之前,我们先看看一个应用了redux-thunk的actionCreator的使用例子:

// actionCreator
export const fetchData = ()=> {
    return dispatch => {
        try {
            dispatch({
                type: 'FETCH_START',
                text: '开始获取数据'
            })
            fetch('/somethingdata').then( data =>{                 
                dispatch({
                    type: 'FETCH_SUCCESS',
                    result: data
                })
            })
        } catch (err) {}
    }
}

我们多次强调,actionCreator返回的必须是action对象,但是这个actionCreator返回的是函数,而且其参数是 dispatch,然后我们在逻辑里发起一个异步请求数据的操作,用 dispatch 发起了两个action,从而支持异步发起多个action的需求,具体为什么返回的是一个参数为dispatch的函数,还要看看redux-thunk的具体实现:

const thunk = ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {    
       return action(dispatch, getState)
    } 
    return next(action)
}
export default thunk

原来,redux-thunk对返回的action进行了判断,如果是function类型,也就代表着需要处理异步逻辑,此时传入redux提供的middlewareAPI,即getState方法和dispatch方法,执行action函数。注意这里的返回值不再是next,这里返回的是actionCreator执行完后的返回值(一般为undefined),所以会中断中间件链的执行,原因很简单,action为函数的时候并非真正发起了action而是为能在action中使用dispatch方法多次发起action。

就是那么简单。。。

middlewareAPI中的dispatch

这里说到了redux提供的middlewareAPI,不知道还记不记得之前说到过,dispatch 有点奇怪:

export default function applyMiddleware(...middlewares) {
 //....
    let dispatch = () => {
      throw new Error(
        `Dispatching while constructing your middleware is not allowed. ` +
          `Other middleware would not be applied to this dispatch.`
      )
    }

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

   //.....
    return{
        ...store,
        dispatch
    }
  }
}

看到dispatch变量,其实是作为一个闭包变量返回的。变量有两个赋值的时候,第一次赋值是定义了一个函数,直接抛出了一个错误,第二次则是通过 compose(...chain)(store.disaptch ) 赋值,也就是我们熟悉的增强过后的 dispatch 赋值,那什么要多此一举初始化了一个错误的输出函数呢?查看了redux的issuse后我得到了答案:

// 在redux最早的版本中,redux提供的middlewareAPI是这样的
 const middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => store.dispatch(action)
}// 看到不同了吗,是直接使用的原生store提供的dispatch

const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)

但是这样做就会出现一个问题,例如有个中间件是这样的:

const errorMiddle = ({ dispatch, getState }) => {
    //发起action
    dispatch({
        type: 'ANY_TYPE_ACTION'
    })

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

这个中间件发起的action不会经过中间件调用链传递,而是直接使用store.dispatch()方法发起,原因很简单:

// 上面的中间件发生错误的原因是因在初始化chain的时候发起了action
// 此时的dispatch 还没有形成中间件调用链条,因而不能经过其他中间件而直接发起action
const chain = middlewares.map(middleware => middleware(middlewareAPI))

// 此时才会形成中间件调用链 
dispatch = compose(...chain)(store.dispatch)

为了避免action不经过中间件传递的错误,redux修改了dispatch,也就是我们现在看到的样子:

    let dispatch = () => {
      throw new Error(
        `Dispatching while constructing your middleware is not allowed. ` +
          `Other middleware would not be applied to this dispatch.`
      )
    }
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }

这样子,在中间件调用链未初始化完成之前,调用dispatch就会报错误提示。

redux-logger

最后我们再来看一个常用的中间件,redux-logger,其作用是输出action发起前和发起后,state树的值。

// 源码过于复杂
// 这里直接简化成如下形式:
let logger = ({ dispatch, getState }) => next => action => {
    console.log('next:之前state', getState())
    let result = next(action)
    console.log('next:之前state', getState())
    return result
}

redux-logger 的任务是输出action发起前和发起后,state树的值。它不关心action经过中间件链条时,发生了什么变化,它只关注结果,这也是为什么它的顺序要放在最后面的原因,例如,我们在实际开发中常常这样使用中间件:

const store = createStore(
    reducer,
    applyMiddleware([redux_thunk,promise,redux_logger])
)

action在传递中间件的过程中,直到最后一个中间件(上文也就是redux_logger)时,才会应用原生的dispatch方法发起,此时才能看到state在action前后的变化,这也就是redux-logger为什么要放在后面的原因了。

最后

Redux源码解读就到此结束了,总体来说,整个框架的设计非常的简洁易读,不禁感叹作者之强了,将flux思想转换为redux思想,最终写出一个易用的框架。在实际开发中,通常不直接使用redux作为react的扩展,而是会使用react-redux这个库,正如redux所言,redux并非为react而生,react-redux充当了兼容二者的关系,但其本源是通过react的context,提供一个全局的provider组件包裹整个app应用,将redux定义在provider上,再通过封装context获取数据,更新数据的逻辑来使得在react任一个组件中都能非常简洁地使用redux。

其实我们也可以看到,react-redux实际上是基于react的context设计的,是否有这样一天,react的context会变得更为强大简洁,使得在react就可以应用redux思想,实现状态管理,而不依赖其他的第三库呢?:)

@Lemonreds Lemonreds changed the title [2018-08-26]: 阅读Redux源码05 - combineReducers [2018-08-26]: 阅读Redux源码05 - applyMiddleware和compose Aug 27, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant