Lemonreds / snippets

code snippets.
https://github.com/Lemonreds/snippets/issues
2 stars 0 forks source link

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

Open Lemonreds opened 6 years ago

Lemonreds commented 6 years ago

首先我们来看看中间件的定义: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) 

参数

方法返回

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思想,实现状态管理,而不依赖其他的第三库呢?:)