函數的柯里化與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

大家的打賞是我寫做的動力

微信
支付寶
相關文章
相關標籤/搜索