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

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

查看demoreact

查看源码,欢迎stargit

高阶函数

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

  • 函数能够做为参数
  • 函数能够做为返回值

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

函数的柯里化

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

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

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

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

大家的打赏是我写做的动力

微信
支付宝
相关文章
相关标签/搜索