ReduxのMiddlewareについて理解したいマン
2018/09/08
ReduxのMiddlewareの仕組みがよく分からない。
具体的な処理過程を追いかけて理解に至るまでのメモ。
Middleware?
ReduxではStore(正確にはStoreのdispatchかな)を拡張する仕組みとしてMiddlewareというものが用意されている。
考え方はNodeのフレームワークExpressのMiddlewareや、RubyのRackと同じと思ってる。
このMiddlewareを使ってロギング機能や非同期処理機能を搭載することができる。
applyMiddleware
ReduxではapplyMiddlewareという関数を使ってStoreにMiddlewareたちを積むことができる。
定義は下記の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
export default function applyMiddleware(...middlewares) { return (next) => (reducer, initialState) => { var store = next(reducer, initialState) var dispatch = store.dispatch var chain = [] var middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) } chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch } } } |
↑の処理内容をざっくり言うと、Middleware群でstoreのdispatchをマトリョーシカ人形の如く包んでる。
Middlewareの中にMiddleware、そのMiddlewareの中にMiddleware、、、最後にdispatch。
こうすることで、Redux本来のdispatchが動く前にMiddlewareたちの処理が実行されるわけ。
コード読む
applyMiddlewareは下記のように呼び出される。
1 |
creatStoreWithMiddleware = applyMiddleware(logger, awesomeMiddleWare)(createStore) |
これでMiddlewareに包まれたcreateStore、createStoreWithMiddlewareができる。
applyMiddleware定義をステップ毎にゆっくり見てゆく。
applyMiddlewareに...middlewares
を与えると、next
を引数にとる関数が返ってくる。
で、この『next引数にとる関数』にcreateStoreを突っ込んでる。
次はMiddlewareを活躍させるための下ごしらえをやってる。
1 2 3 4 5 6 7 8 9 10 11 12 |
// nextはcreateStoreなので、まさにStoreを生成する処理 var store = next(reducer, initialState) // オリジナルのdispatchの参照を保持しておく var dispatch = store.dispatch var chain = [] // 各Middleware内でオリジナルstoreのgetStateとdispatchを使えるようにするためのオブジェクト // このmiddlewareAPIのおかげで、Middleware内で非同期処理開始する前にdispatch(非同期処理開始するよAction)とか、非同期処理完了後にdispatch(ネットからオブジェクト取ってきたよAction)といったことができる var middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) } |
次。
1 |
chain = middlewares.map(middleware => middleware(middlewareAPI)) |
見てのとおり、chainにはMiddlewareにmiddlewareAPIを与えた返り値リストが格納されてる。
繰り返しになるけど、middlewareAPIはStoreのgetStateとdispatchへの参照を持ったオブジェクト。
このオブジェクトを与えることで、Middlewareの中からgetStateとdispatchを呼び出すことができる。
なので、各Middlewareは引数としてmiddlewareAPIオブジェクトが与えられることを想定しておく必要なある。
たとえば、redux-thunkという非同期処理を行うためのMiddlewareはこんな感じだ。
1 2 3 4 5 6 |
function thunkMiddleware({ dispatch, getState }) { return next => action => typeof action === 'function' ? action(dispatch, getState) : next(action); } |
{ dispatch, getState }と、middlewareAPIを受け取ってることが分かる。
ちなみにMiddleware(redux-thunk)にmiddlewareAPIを与えると次のような関数が返ってくる。
1 2 3 4 5 |
return function(next) { return function(action) { return typeof action === 'function' ? action(dispatch, getState) : next(action); } } |
なので、chainは『オリジナルのStoreへの参照を持った関数を返す関数』のリスト、となる。
で、このchainをcomposeというシロモノを使い1つの関数にまとめる。
1 |
dispatch = compose(...chain)(store.dispatch) |
composeはReduxのユーティリティ。
これは関数のリストを受け取って合成関数を返すシロモノ。
なので『関数を返す関数』たちの合成関数ができあがる。
ここで、compose(…chain)の具体的な処理を冗長に追ってみる。
composeはこれね。
1 2 3 4 5 6 7 8 9 10 11 12 |
export default function compose(...funcs) { return (...args) => { if (funcs.length === 0) { return args[0] } const last = funcs[funcs.length - 1] const rest = funcs.slice(0, -1) return rest.reduceRight((composed, f) => f(composed), last(...args)) } } |
chainは2つのMiddlewareを抱えるリストで…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
chain = [ return function(next) { return function(action) { console.log('before') next(action) console.log('after') } }, return function(next) { return function(action) { return typeof action === 'function' ? action(dispatch, getState) : next(action); } } ] |
と、する。
それぞれログを吐くMiddleware、非同期処理をやるためのMiddleware(redux-thunk)。
composeの中身はシンプルで、合成関数の作成はreduceRightを使って実現してることが分かる。
1 |
return rest.reduceRight((composed, f) => f(composed), last(...args)) |
まずreduceRightの初期値、last(…args)はchainの最後の関数にargsを与えたモノ。
なので、nextにargsが入る。
1 2 3 |
reduceRightの初期値 = function(action) { return typeof action === 'function' ? action(dispatch, getState) : args(action); } |
で、この初期値がcomposedになるので、次の関数fの引数として与える。
次の関数fは…
1 2 3 4 5 6 7 |
return function(next) { return function(action) { console.log('before') next(action) console.log('after') } } |
なので
1 2 3 4 5 |
f(reduceRightの初期値) = function(action) { console.log('before') reduceRightの初期値(action) console.log('after') } |
で、次の関数は存在しないのでreduceRightの処理はおしまい。
compose(..chain)の返り値は下記のようになる。
1 2 3 4 5 6 7 |
function(...args) { return function(action) { console.log('before') reduceRightの初期値(action) console.log('after') } } |
ログを吐くMiddleware -> 非同期処理を行うMiddleware(redux-thunk)という順番で処理が実行されることが分かるかと思う。
applyMiddlewareを見返すと…
1 2 |
chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch) |
composeで返ってきた関数にstore.dispatchを与えてる。
1 2 3 4 5 6 7 8 9 10 11 |
reduceRightの初期値 = function(action) { return typeof action === 'function' ? action(dispatch, getState) : args(action); } // これが… reduceRightの初期値 = function(action) { return typeof action === 'function' ? action(dispatch, getState) : store.dispatch(action); } // こうなる |
Middlewareの層の最下層にstore.dispatchが待ち構えている。
Storeのdispatch機能を保持したまま、Middleware群によって機能拡張されてる。
1 2 3 4 |
return { ...store, dispatch } |
最後にオリジナルのstore(createStore)オブジェクトのdispatchを拡張dispatchでオーバーライドしたオブジェクトを返してる。
storeは独自のStore作成関数を使わないかぎりはreduxのcreateStoreになるかと。
これでMiddlewareを搭載したStoreができた。