zoukankan      html  css  js  c++  java
  • 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)
                    }
                }
            }
        }
    

      

  • 相关阅读:
    「Vijos 1282」「OIBH杯NOIP2006第二次模拟赛」佳佳的魔法照片
    「Vijos 1285」「OIBH杯NOIP2006第二次模拟赛」佳佳的魔法药水
    「Vijos 1284」「OIBH杯NOIP2006第二次模拟赛」佳佳的魔法阵
    「Vijos 1283」「OIBH杯NOIP2006第二次模拟赛」佳佳的魔杖
    「2018-12-02模拟赛」T3 约束排列 解题报告
    「2018-12-02模拟赛」T2 种树 解题报告
    「2018-12-02模拟赛」T1 最短路 解题报告
    「分块系列」公主的朋友 解题报告
    「分块系列」「洛谷P4168 [Violet]」蒲公英 解题报告
    Java高级架构师(一)第03节:多模块多Web应用合并War包
  • 原文地址:https://www.cnblogs.com/fuGuy/p/11527539.html
Copyright © 2011-2022 走看看