函数的柯里化与Redux中间件及applyMiddleware源码分析

xiaozaq 2019-06-28

奇怪,怎么把函数的柯里化和Redux中间件这两个八竿子打不着的东西联系到了一起,如果你和我有同样疑问的话,说明你对Redux中间件的原理根本就不了解,我们先来讲下什么是函数的柯里化?再来讲下Redux的中间件及applyMiddleware源码

查看demo

查看源码,欢迎star

高阶函数

提及函数的柯里化,就必须先说一下高阶函数(high-order function),高阶函数是满足下面两个条件其中一个的函数:

  • 函数可以作为参数
  • 函数可以作为返回值

看到这个,大家应该秒懂了吧,像我们平时使用的setTimeout,map,filter,reduce等都属于高阶函数,当然还有我们今天要说的函数的柯里化,也是高阶函数的一种应用

函数的柯里化

什么是函数的柯里化?看过JS高程一书的人应该知道有一章是专门讲JS高级技巧的,其中对于函数的柯里化是这样描述的:

它用于创建已经设置好了一个或多个参数的函数。函数的柯里化的基本使用方法和函数绑定是一样的:使用一个闭包返回一个函数。两者的区别在于,当函数被调用时,返回的函数还需要设置一些传入的参数

听得有点懵逼是吧,来看一个例子

const add = (num1, num2) => {
    return num1 + num2
}

const sum = add(1, 2)

add是一个返回两个参数和的函数,而如果要对add进行柯里化改造,就像下面这样

const curryAdd = (num1) => {
    return (num2) => {
        return num1 + num2
    }
}
const sum = curryAdd(1)(2)

更通用的写法如下:

const curry = (fn, ...initArgs) => {
    let finalArgs = [...initArgs]
    return (...otherArgs) => {
        finalArgs = [...finalArgs, ...otherArgs]
        if (otherArgs.length === 0) {
            return fn.apply(this, finalArgs)
        } else {
            return curry.call(this, fn, ...finalArgs)
        }
    }
}

我们在对我们的add进行改造来让它可以接收任意个参数

const add = (...args) => args.reduce((a, b) => a + b)

再用我们上面写的curry对add进行柯里化改造

const curryAdd = curry(add)

curryAdd(1)
curryAdd(2, 5)
curryAdd(3, 10)
curryAdd(4)
const sum = curryAdd() // 25

注意我们最后必须调用curryAdd()才能返回操作结果,你也可以对curry进行改造,当传入的参数的个数达到fn指定的参数个数就返回操作结果

总之函数的柯里化就是将多参数函数转换成单参数函数,这里的单参数并不仅仅指的是一个参数,我的理解是参数切分

PS:敏感的同学应该看出来了,这个和ES5的bind函数的实现很像。先来一段我自己实现的bind函数

Function.prototype.bind = function(context, ...initArgs) {
    const fn = this
    let args = [...initArgs]
    return function(...otherArgs) {
        args = [...args, ...otherArgs]
        return fn.call(context, ...args)
    }
}

var obj = {
    name: 'monkeyliu',
    getName: function() {
        console.log(this.name)
    }
}

var getName = obj.getName
getName.bind(obj)() // monkeyliu

高程里面这么评价它们两个:

ES5的bind方法也实现了函数的柯里化。使用bind还是curry要根据是否需要object对象响应来决定。它们都能用于创建复杂的算法和功能,当然两者都不应滥用,因为每个函数都会带来额外的开销

Redux中间件

什么是Redux中间件?我的理解是在dispatch(action)前后允许用户添加属于自己的代码,当然这种理解可能并不是特别准确,但是对于刚接触redux中间件的同学,这是理解它最好的一种方式

我会通过一个记录日志和打印执行时间的例子来帮助各位从分析问题到通过构建 middleware 解决问题的思维过程

当我们dispatch一个action时,我们想记录当前的action值,和记录变化之后的state值该怎么做?

手动记录

最笨的办法就是在dispatch之前,打印当前的action,在dispatch之后打印变化之后的state,你的代码可能是这样

const action = { type: 'increase' }
console.log('dispatching:', action)
store.dispatch(action)
console.log('next  state:', store.getState())

这是一般的人都会想到的办法,简单,但是通用性较差,如果我们在多处都要记录日志,上面的代码会被写多次

封装Dispatch

要想复用我们的代码,我们会尝试封装下将上面那段代码封装成一个函数

const dispatchAndLog = action => {
    console.log('dispatching:', action)
    store.dispatch(action)
    console.log('next  state:', store.getState())
}

但是这样的话只是减少了我们的代码量,在需要用到它的地方我们还是得每次引入这个方法,治标不治本

改造原生的dispatch

直接覆盖store.dispatch,这样我们就不用每次引入dispatchAndLog,这种办法网上人称作monkeypatch(猴戏打补),你的代码可能是这样

const next = store.dispatch
store.dispatch = action => {
    console.log('dispatching:', action)
    next(action)
    console.log('next  state:', store.getState())
}

这样已经能做到一次改动,多处使用,已经能达到我们想要的目的了,但是,it's not over yet(还没结束)

记录执行时间

当我们除了要记录日志外,还需要记录dispatch前后的执行时间,我们需要新建另外一个中间件,然后依次去执行这两个,你的代码可能是这样

const logger = store => {
    const next = store.dispatch
    store.dispatch = action => {
        console.log('dispatching:', action)
        next(action)
        console.log('next  state:', store.getState())
    }
}

const date = store => {
    const next = store.dispatch
    store.dispatch = action => {
        const date1 = Date.now()
        console.log('date1:', date1)
        next(action)
        const date2 = Date.now()
        console.log('date2:', date2)
    }
}

logger(store)
date(store)

但是这样的话,打印结果如下:

date1: 
dispatching: 
next  state: 
date2:

中间件输出的结果和中间件执行的顺序相反

利用高阶函数

如果我们在logger和date中不去覆盖store.dispatch,而是利用高阶函数返回一个新的函数,结果又是怎样呢?

const logger = store => {
    const next = store.dispatch
    return action => {
        console.log('dispatching:', action)
        next(action)
        console.log('next  state:', store.getState())
    }
}

const date = store => {
    const next = store.dispatch
    return action => {
        const date1 = Date.now()
        console.log('date1:', date1)
        next(action)
        const date2 = Date.now()
        console.log('date2:', date2)
    }
}

然后我们需要创建一个函数来接收logger和date,在这个函数体里面我们循环遍历它们,将他们赋值给store.dispatch,这个函数就是applyMiddleware的雏形

const applyMiddlewareByMonkeypatching = (store, middlewares) => {
    middlewares.reverse()
    middlewares.map(middleware => {
        store.dispatch = middleware(store)
    })
}

然后我们可以这样应用我们的中间件

applyMiddlewareByMonkeypatching(store, [logger, date])

但是这样仍然属于猴戏打补,只不过我们将它的实现细节,隐藏在applyMiddlewareByMonkeypatching内部

结合函数柯里化

中间件的一个重要特性就是后一个中间件能够使用前一个中间件包装过的store.dispatch,我们可以通过函数的柯里化实现,我们将之前的logger和date改造了下

const logger = store => next => action => {
    console.log('dispatching:', action)
    next(action)
    console.log('next  state:', store.getState())
}

const date = store => next => action => {
    const date1 = Date.now()
    console.log('date1:', date1)
    next(action)
    const date2 = Date.now()
    console.log('date2:', date2)
}

redux的中间件都是上面这种写法,next为上一个中间件返回的函数,并返回一个新的函数作为下一个中间件next的输入值

为此我们的applyMiddlewareByMonkeypatching也需要被改造下,我们将其命名为applyMiddleware

const applyMiddleware = (store, middlewares) => {
    middlewares.reverse()
    let dispatch = store.dispatch
    middlewares.map(middleware => {
        dispatch = middleware(store)(dispatch)
    })
    return { ...store, dispatch }
}

我们可以这样使用它

let store = createStore(reducer)

store = applyMiddleware(store, [logger, date])

这个applyMiddleware就是我们自己动手实现的,当然它跟redux提供的applyMiddleware还是有一定的区别,我们来分析下原生的applyMiddleware的源码就可以知道他们之间的差异了

applyMiddleware源码

直接上applyMiddleware的源码

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args)
    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
    }
  }
}

原生的applyMiddleware是放在createStore的第二个参数,我们也贴下createStore的相关核心代码,然后结合二者一起分析

export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    return enhancer(createStore)(reducer, preloadedState)
  }
  ....
}

当传入了applyMiddleware,此时最后执行enhancer(createStore)(reducer, preloadedState)并返回一个store对象,enhancer就是我们传入的applyMiddleware,我们先执行它并返回一个函数,该函数带有一个createStore参数,接着我们继续执行enhancer(createStore)又返回一个函数,最后我们执行enhancer(createStore)(reducer, preloadedState),我们来分析这个函数体内做了些什么事?

const store = createStore(...args)

首先利用reducer和preloadedState来创建一个store对象

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

这句代码的意思就是在构建中间件的过程不可以调用dispath函数,否则会抛出异常

const middlewareAPI = {
  getState: store.getState,
  dispatch: (...args) => dispatch(...args)
}

定义middlewareAPI对象包含两个属性getState和dispatch,该对象用来作为中间件的输入参数store

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

chain是一个数组,数组的每一项是一个函数,该函数的入参是next,返回另外一个函数。数组的每一项可能是这样

const a = next => {
    return action => {
        console.log('dispatching:', action)
        next(action)
    }
}

最后几行代码

dispatch = compose(...chain)(store.dispatch)
return {
  ...store,
  dispatch
}

其中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)))
}

compose是一个归并方法,当不传入funcs,将返回一个arg => arg函数,当funcs长度为1,将返回funcs[0],当funcs长度大于1,将作一个归并操作,我们举个例子

const func1 = (a) => {
  return a + 3
}

const func2 = (a) => {
  return a + 2
}

const func3 = (a) => {
  return a + 1
}

const chain = [func1, func2, func3]

const func4 = compose(...chain)

func4是这样的一个函数

func4 = (args) => func1(func2(func3(args)))

所以上述的dispatch = compose(...chain)(store.dispatch)就是这么一个函数

const chain = [logger, date]
dispatch = compose(...chain)(store.dispatch)
// 等价于
dispatch = action => logger(date(store.dispatch))

最后在把store对象传递出去,用我们的dispatch覆盖store中的dispatch

return {
    ...store,
    dispatch
}

到此整个applyMiddleware的源码分析完成,发现也没有想象中的那么神秘,永远要保持一颗求知欲

和手写的applyMiddleware的区别

差点忘记了这个,讲完了applyMiddleware的源码,在来说说和我上述自己手写的applyMiddleware的区别,区别有三:

  • 原生的只提供了getState和dispatch,而我手写的提供了store中所有的属性和方法
  • 原生的middleware只能应用一次,因为它是作用在createStore上;而我自己手写的是作用在store上,它可以被多次调用
  • 原生的可以在middleware中调用store.dispatch方法不产生任何副作用,而我们手写的会覆盖store.dispatch方法,原生的这种实现方式对于异步的middle非常有用

最后

查看demo

查看源码,欢迎star

你们的打赏是我写作的动力

<img alt='微信' src='https://user-gold-cdn.xitu.io...;h=1280&f=webp&s=39482' width='200' />

<img alt='支付宝' src='https://user-gold-cdn.xitu.io...;h=1350&f=webp&s=45674' width='200'/>

相关推荐

jiangcs0 / 0评论 2020-04-19

空谷足音 / 0评论 2019-04-28