函式的柯里化與Redux中介軟體及applyMiddleware原始碼分析

劉源泉發表於2019-03-01

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

你們的打賞是我寫作的動力

微信
支付寶

相關文章