React Hooks 實現react-redux


Redux 是目前 React 系統中最常用的數據管理工具,它落實並發揚了 Flux 的數據單向流動模式,被實踐證明為一種成熟可用的模式。

盡管承受着一些非議,Redux 在 React 數據管理界的地位仍然沒有被取代。我聽到的針對 Redux 最多的非議是它需要遵守的規則和步驟太多,讓人們覺得束手束腳。然而個人覺得這正是 Redux 的意思所在,項目大了,如果整體數據流動不遵守規則,就容易亂。數據單向流動模式配合 Redux 規范仍然是一個可行的方案,當然,完全擁抱 mutable 的 Mobx 和 Vuex 也有他們的優勢,關於他們之間的對比這里暫且不多做介紹。今天我們的重點是制作我們自己的 Redux,從而深入了解它的思想和原理。

 

 

Redux 的主要思想是讓系統中的數據按照統一的規則流動,這樣所有的操作就都有跡可循,任何 View 上對系統數據狀態的更改都要通過 Action 被 Dispatch 到 Store,通過 Reducer 定義的邏輯去更改 State,然后再去更新View。

我們可以看到 Redux 的核心是 Store,我們就從它入手來寫自己的 Redux。Store 需要有 State,需要提供 Dispatch 方法來接收 Action,需要根據使用者提供的 Reducer 響應 Action,還要能夠在 State 變化的時候通知到外部的觀察者。我們先來看看典型的 Action 和 Reducer:

Action:

  {
      type: ADD_TODO,
      text: 'Build my first Redux app'
    }

Reducer:

    function todoApp(state, action) {
      switch(action.type) {
        case ADD_TODO:
          return Object.assign({}, state, {
            todos: [
              ...state.todos,
              { text: action.text, completed: false }
            ]
          })
        default:
          return state
      }
    }

  

我們可以看到,Action 是普通的對象,它需要有一個 type 屬性來指示要做的動作類型,Reducer 是一個方法,接收當前的 State 和要做的 Action,定義邏輯給出新的 State。

通過應用閉包和觀察者模式,我們的 Redux 核心 createStore 方法並不難寫:

export default function(reducer) {
        let state = undefined
        let listeners = []

        function subscribe(listener) {
            listeners.push(listener)
            return function unsubscribe() {
                listeners.splice(listeners.indexOf(listener), 1)
            }
        }

        function dispatch(action) {
            state = reducer(state, action)
            listeners.map(listener => listener())
        }

        function getState() {
            return state
        }

        return { dispatch, subscribe, getState }
    }

  有了核心部分,接下來是如何讓 View 也就是 React components 拿到 state,能夠在 state 更新的時候被更新,以及能夠使用 dispatch 方法來發出 Action 指令。Redux 需要使用者將 App 包裹在 Redux 的 Provider 中,提供自己創建的 Store 作為屬性,再讓使用者用 connect 來包裹 component 為它拿到 state 數據和 dispatch 方法:

 const store = createStore(rootReducer)
    ReactDOM.render((
        <Provider store={store}>
            <App />
        </Provider>
    ), document.getElementById('root'))


    function mapStateToProps(state) {
        return { count: state.count }
    }
    function mapDispatchToProps(dispatch) {
        return {
            increment: () => dispatch({type: 'increment'})
        }
    }
    export default connect(mapStateToProps, mapDispatchToProps)(Counter)

  

為了讓 Component Tree 中任何位置的 Component 都能接收到信息,我們需要 React Context 的幫助,我們的 Provider 將使用者生成的 Store 注入 Context,connect 方法為 Component 拿到相應 Context 中的 Store 信息。以前的 React Context 在 Context 更新時不能確保系統中所有對 Context 的引用都得到更新,詳細情況可以參考 https://reactjs.org/docs/legacy-context.html#updating-context ,因而當時實現 State 更新的方式比較復雜,每個 Component 都需要獨立訂閱 Store。新的 React Context API 解決了 Context 更新問題,我們寫起來也就容易了很多,只要在 Provider 中訂閱更新就可以了:

Provider:

 import React, { useState, useEffect } from 'react'

    export const MyReduxContext = React.createContext()

    export default function(props) {
        const { store } = props
        const [ provider, setProvider ] = useState({state: store.getState(), dispatch: store.dispatch})

        useEffect(() => {
            return store.subscribe(() => setProvider({state: store.getState(), dispatch: store.dispatch}))
        })

        return (
            <MyReduxContext.Provider value={provider}>
                { props.children }
            </MyReduxContext.Provider>
        )
    }

Connect:

   import React from 'react'
    import { MyReduxContext } from './Provider';

    export default function(mapStateToProps, mapDispatchToProps) {
        return function(WrappedComponent) {
            return function(props) {
                return (
                    <MyReduxContext.Consumer>
                        {
                            ({ state, dispatch }) => {
                                return <WrappedComponent {...mapStateToProps(state, props)} {...mapDispatchToProps(dispatch, props)} />
                            }
                        }
                    </MyReduxContext.Consumer>
                )
            }
        }
    }

  

這里我們用到了 React Hooks 的 useState 和 useEffect,注意 state 和 dispatch 都需要被放在 Context 中傳遞。connect 方法的兩個參數 mapStateToProps 和 mapDispatchToProps,正如他們的名字,是用來把 state 和 dispatch 轉化成 Component 需要的樣子。

Connect 方法是現在 Redux 提供的連接 Component 的方式,然而現在我們有了 React Hooks,是否有辦法自定義 Hooks 來連接呢?借用 useContext 我們可以很簡單地實現:

   import { useContext } from 'react'
    import { MyReduxContext } from './Provider'

    export default function useStore() {
        const { state, dispatch } = useContext(MyReduxContext)
        return [ state, dispatch ]
    }

  使用時也很簡單:

 const [ state, dispatch ] = useStore()
 const { count } = mapStateToProps(state)
 const { increment, reset } = mapDispatchToProps(dispatch)

  

到此為止我們的 Redux 主流程工具已經完成,接下來我們來看 Redux 的重要概念 Middleware。Middleware是從 Redux 的流程規則中應運而生的,既然所有的 Action 都要流經 Store,那如果我們在 Store 中設置一個可插入的裝置,就可以讓人們根據需要加入各種管道方法,最常見的有記錄日志,報告錯誤,異步請求處理和路由等。

這個裝置需要將 middleware 們一個個插入到 Store 的 Dispatch 管道中,讓 Action 一個個地流經他們,最后才被真正的 Dispatch 給到 Reducer。為了靈活可靠地完成這個任務,我們的 Redux 需要做很多讓人頭暈的工作,准備好迎接挑戰了嗎?

首先,為了將 middleware 們組合起來,我們需要將他們的流程邏輯嵌套在一起成為一個新的 Dispatch 方法,這就需要用到 Compose 方法。

  d = compose(a, b, c)
  d(x) === a(b(c(x)))

我們的 middleware 如果長這樣:

  function middlewareWrapper(nextDispatch) {
        return function(action) {
            some logic ...
            nextDispatch(action);
        }
    }

  

那么想象一下 compose(...middlewares)(store.dispatch) 會得到什么?帶入上面的 compose 方法的定義仔細想一下。沒錯,就是 middleware 們內層函數的邏輯嵌套,等待被執行的一個新的 Dispatch 方法。

Compose 並不難實現,我們可以自己簡單寫一下。

 function compose(...funcs) {
        return function(...parameters) {
            let returned = null
            for (let i = funcs.length - 1; i >= 0; i --) {
                let func = funcs[i]
                if (i === 0) {
                    returned = func(...parameters)
                } else {
                    returned = func(returned)
                }
            }
            return returned
        }
    }

  知道了如何嵌套 middleware,我們就可以着手改造我們的 createStore,將 middlewares 作為額外參數傳入。

export default function createStore(reducer, middlewares) {
        let listeners = []
        function subscribe(listener) { }
        function dispatch(action) {
            state = reducer(state, action)
            listeners.map(listener => listener())
        }
        function getState() { return state }
    ------
        const storeContext = {
            getState: getState,
            dispatch: (action) => dispatch(action)
        }

        const chain = middlewares.map(middleware => middleware(storeContext))

        dispatch = compose(...chain)(store.dispatch)
    ------
        return { dispatch, subscribe, getState }
    }

  這里將 storeContext 傳給 middleware 是希望它們能夠拿到 store 的 state 和 dispatch,注意這里的 dispatch 指向的是嵌套后的新的 dispatch,既然多了一步 storeContext 的封裝,middleware 們也就又多了一層包裹,最終變成這樣:

 function storeWrapper(store) {
        function middlewareWrapper(nextDispatch) {
            return function(action) {
                some logic ...
                nextDispatch(action)
            }
        }
    }

  

到此為止 middleware 的內部機制我們已經做好了,可是 Redux 傳入 middlewares 的方式並不是這樣的,為了使用起來更靈活,Redux 提供了 applyMiddleware 方法,它接收 middlewares 作為參數,返回一個接收 createStore 參數的包裝方法,將 createStore 包裝為一個新的 createStore 方法,新的方法給出的 dispatch 方法就是嵌套好 middlewares 的新的 dispatch。

因而我們的 Provider 處可以寫做:

 <Provider store={applyMiddleware(loggerMiddleware, thunkMiddleware)(createStore)(counterReducer)}>

  而從另一個角度,原有的 createStore 方法也支持接收 applyMiddleware 的返回值作為 enhancer 參數,而且還需要有另一個參數 preloadedState 作為 state 的初始值,最終就變成了:

 <Provider store={createStore(counterReducer, preloadedState, applyMiddleware(loggerMiddleware, thunkMiddleware))}>

  我們的 createStore 方法最終是這樣的:

 export default function createStore(reducer, preloadedState, enhancer) {
        if (enhancer) {
            return enhancer(createStore)(reducer, preloadedState)
        }
    ------
        let state = preloadedState
        let listeners = []
        function subscribe(listener) {
            listeners.push(listener)
            return function unsubscribe() {
              listeners.splice(listeners.indexOf(listener), 1)
            }
        }
        function dispatch(action) {
            if (typeof action !== 'object') {
                return;
            }
            state = reducer(state, action)
            listeners.map(listener => listener())
        }
        function getState() {
            return state
        }
        return { dispatch, subscribe, getState }
    }

  applyMiddleware 是這樣的:

 export default function applyMiddleware(...middlewares) {

        return function(createStore) {

            return function(reducer, preloadedState, enhancer) {
                const store = createStore(reducer, preloadedState, enhancer)
                // this dispatch used in storeConotext should point to the final dispatch, 
                // or else middlewares will use the real store's dispatch which skips middlewares
                let dispatch = store.dispatch

                const storeContext = {
                    getState: store.getState,
                    dispatch: (action) => dispatch(action)
                }

                const chain = middlewares.map(middleware => middleware(storeContext))

                dispatch = compose(...chain)(store.dispatch)

                // purpose of applyMiddleware is to make a enhanced dispatch which can walk through middlewares
                return {
                    ...store,
                    dispatch
                }
            }
        }
    }

  

這里多次應用了類似 React HOC 的包裝思想,讓整個設計靈活巧妙,但是從某種程度上增加了理解的難度,讓我想到 Redux Saga 的巧妙設計和最初的難以理解。

看這部分時如果有細節不清楚還可以參考 Redux 官方對於 applyMiddleware 的介紹。

有了 applyMiddleware,接下來讓我們寫一下兩個最常用的 middleware —— logger 和 thunk。

初步的 logger 非常簡單,就是增加一個 console 的邏輯:

 export default function loggerMiddleware(storeContext) {
        return function(nextDispatch) {
            return function(action) {
                nextDispatch(action)
                console.log(storeContext.getState())
            }
        }
    }

  

注意,我們把 console.log 放在 nextDispatch 的后面,是希望它拿到此次 dispatch 之后的 state,當然也可以將之前的記錄下來在后面一起 console 出來,就更接近真正的 Redux logger 的做法了。

Redux thunk 是要讓 dispatch 接受 Action Creator 作為參數,詳細內容可以參考 https://github.com/reduxjs/redux-thunk#why-do-i-need-this ,它的實現也很簡單,只要做一個類型判斷就可以了:

export default function thunkMiddleware(storeContext) {
        return function(nextDispatch) {
            return function(action) {
                if (typeof action === 'function') {
                    const { dispatch, getState } = storeContext
                    action(dispatch, getState)
                    nextDispatch(action)
                } else {
                    nextDispatch(action)
                }
            }
        }
    }

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM