资讯专栏INFORMATION COLUMN

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

jeyhan / 1496人阅读

摘要:函数的柯里化的基本使用方法和函数绑定是一样的使用一个闭包返回一个函数。先来一段我自己实现的函数高程里面这么评价它们两个的方法也实现了函数的柯里化。使用还是要根据是否需要对象响应来决定。

奇怪,怎么把函数的柯里化和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

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



文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/96645.html

相关文章

  • redux middleware 详解

    摘要:执行完后,获得数组,,它保存的对象是图中绿色箭头指向的匿名函数,因为闭包,每个匿名函数都可以访问相同的,即。是函数式编程中的组合,将中的所有匿名函数,,组装成一个新的函数,即新的,当新执行时,,从左到右依次执行所以顺序很重要。 前言 It provides a third-party extension point between dispatching anaction, and t...

    yanwei 评论0 收藏0
  • 十分钟理解Redux间件

    摘要:最后看一下这时候执行返回,如下调用执行循序调用第层中间件返回即调用第层中间件返回即调用根返回即调用一个例子读懂上文提到是个柯里化函数,可以看成是将所有函数合并成一个函数并返回的函数。 由于一直用业界封装好的如redux-logger、redux-thunk此类的中间件,并没有深入去了解过redux中间件的实现方式。正好前些时间有个需求需要对action执行时做一些封装,于是借此了解了下...

    i_garfileo 评论0 收藏0
  • 浅析Redux源码

    摘要:用法源码由在年创建的科技术语。我们除去源码校验函数部分,从最终返回的大的来看。这个返回值无法被识别。洋葱模型我们来看源码源码每个都以作为参数进行注入,返回一个新的链。改变原始组数,是一种副作用。 @(Redux)[|用法|源码] Redux 由Dan Abramov在2015年创建的科技术语。是受2014年Facebook的Flux架构以及函数式编程语言Elm启发。很快,Redux因其...

    lifesimple 评论0 收藏0
  • Redux源码分析

    摘要:在得到新的状态后,依次调用所有的监听器,通知状态的变更。执行完后,获得数组,它保存的对象是第二个箭头函数返回的匿名函数。部分源码利用这个属性,所有子组件均可以拿到这个属性。 Redux使用中的几个点: Redux三大设计原则 Create Store Redux middleware combineReducer Provider与Connect Redux流程梳理 Redux设计特...

    renweihub 评论0 收藏0
  • Redux:Middleware你咋就这么难

    摘要:接下来的函数就有点难度了,让我们一行一行来看。上面实际的含义就是将数组每一个执行的返回值保存的数组中。需要注意的是,方法返回值并不是数组,而是形如初始值的经过叠加处理后的操作。从而实现异步的。   这段时间都在学习Redux,感觉对我来说初学难度很大,中文官方文档读了好多遍才大概有点入门的感觉,小小地总结一下,首先可以看一下Redux的基本流程:showImg(https://segm...

    superPershing 评论0 收藏0

发表评论

0条评论

jeyhan

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<