乐趣区

关于前端:详解ReduxReactredux及Redux中间件

Redux 构造

有想过本人去实现一个 Redux 吗?其实并不难,Redux次要就是由 storereduceraction 组成的,接下来咱们一步一步来尝试的搭建Redux

Redux 的分步实现

reducer

依据下面图外面的介绍咱们晓得 reducer 就是依据传来的 type,对相干state 进行解决,之后返回一个新的state。由此咱们失去以下代码:

// reducer.js
const init = {num: 0}

export const reducer = (state = init, action) => {switch(action.type) {
        case 'add': 
            return {
                ...state,
                num: state.num + 1
            }
        case 'low':
            return {
                ...state,
                num: state.num - 1
            }
        default:
            return init
    }

}

store

咱们实现下面图里的第一步,就是 store.js 文件。咱们首先须要明确 store 文件次要有三个重要的函数,别离是subscribe,dispatch,getState。接下来间接贴上代码来剖析吧。

// store.js
import {reducer} from './reducer.js'

export const createStore = () => {let currentState = {}
    let collect = []
    dispatch({})

    function getState() {return currentState}

    function dispatch(action) {currentState =  reducer(currentState, action)
        collect.forEach(tempFunc => tempFunc())
    }

    function subscribe(tempFunc) {if (fn instanceof Function) {collect.push(tempFunc)
        }
        return
    }
  
    return {getState, dispatch, subscribe}
}

咱们能够看到 createStore 函数中除了三个根本函数之外有一行 dispatch({}) 这个其实就是为了初始化redux,如果不触发reducer 外面的初始化的话,如果对相干值进行 操作就会失去一个 NaN 的值。

而后 subscribe 函数次要就是依据观察者模式实现的,当用户在页面订阅 subscribe 函数,接着在进行 dispatch 操作之后就会触发以后页面所有订阅 subscribe 的函数。这么讲很麻烦,上代码吧。

// index.js
import React from 'react'
import {createStore} from '../../store/store'
import {reducer} from '../../store/reducer'

const store = createStore(reducer)  
export class Roll extends React.Component {constructor(props) {super(props)
        this.state = {num:0}
    }

    componentWillMount() {store.subscribe(()=>this.setState({num: store.getState().num
        }))
    }
    lowNum() {store.dispatch({ type: 'low'})
        console.log('store 外面的值为' + store.getState().num)
    }
    addNum() {store.dispatch({ type: 'add'})
        console.log('store 外面的值为' + store.getState().num)
    }
    render() {
        return (<div style={{ textAlign:'center', paddingTop:'100px'}}>
                <button onClick={()=> this.lowNum()}>low</button>
                <div style={{display: 'inline', padding:'0 10px'}}>{this.state.num}</div>
                <button onClick={()=> this.addNum()}>add</button>
            </div>
        )
    }
}

加上了 subscribe 函数的效果图:

没加 subscribe 函数的效果图:

没加的话理论就是更新了 store 外面的状态,然而 store 的状态未同步到页面来,从而无奈触发页面的更新。

react-redux 的实现

咱们个别是在 react 我的项目里并不会间接去应用 redux,而是利用 react-redux 作为沟通两者的桥梁。

例子

首先咱们看看 react-redux 的简略应用形式。

// Provider 伪代码
ReactDOM.render(<Provider store={store}>
        <ChildComponent />
    </Provider>
)

//connent 伪代码
ChildComponent = connect(mapStateToProps, mapDispatchToProps)(ChildComponent)

Provider

Provider等同于一个容器组件,容器外部能够嵌套多层组件,实际上 Provider 不会对外面组件做任何解决,只须要让组件失常显示,它承受一个 store 参数,它会把这个外界传来的 store 参数传入到 context 中,而后让这个组件成为组件树的根节点,那么它的子组件都能够获取到 context 了。

// provider.js
import React from 'react'
import PropTypes from 'prop-types'

export class Provider extends React.Component {
    // 申明 Context 对象属性
    static childContextTypes = {
        store: PropTypes.object,
        children: PropTypes.object
    }
    // 返回 Context 对象中的属性
    getChildContext = () => {
        return {store: this.props.store}
    }

    render () {
        return (<div>{this.props.children}</div>
        )
    }
}

Connect

connect函数实际上接管了一个组件作为参数,最初返回一个新的组件,也就是咱们常说的 HOC(高阶组件),它除了接管到一个组件外还接管两个参数,一个是mapStateToProps,还有一个是mapDispatchToProps,这些是传入该组件的props, 须要由connect 这个高阶组件原样传回原组件。咱们大略理解流程了能够简略实现一下:

import React from 'react'
import PropTypes from 'prop-types'

export function connect(mapStateToProps, mapDispatchToProps) {
    // 1. 传入 state 和 dispatch 对象
  return function(WrappedCompment)  {
      // 2. 接管传入的组件
    class Connect extends React.Component {constructor() {super()
            this.state = {
                // 3. 将所有的 props 整合在一个对象上,不便书写
                mapStateAndDispatchProps:{}}
        }
        static contextTypes = {
            // 4. 获取 context 里的 store
            store: PropTypes.object
        }

        componentDidMount() {const { store} = this.context
            // 5. 用于更新和合并几个传入对象
            this.mergeAndUpdateProps()
            store.subscribe(()=> {this.mergeAndUpdateProps()
            })
        }

        mergeAndUpdateProps() {const { store} = this.context
            let tempState = mapStateToProps ? mapStateToProps(store.getState(), this.props) : {}
            let tempDispatch = mapDispatchToProps ? mapDispatchToProps(store.dispatch, this.props) : {}
            this.setState({ 
                mapStateAndDispatchProps : {
                    ...tempState,
                    ...tempDispatch,
                    ...this.props
                }
            })
        }

        render() {
            // 将所有传入的 props 放入之前的组件中
            return <WrappedCompment {...this.state.mapStateAndDispatchProps}/>
        }
    }
    // 返回新组件
    return Connect
}
}

实现成果

接入到 Roll 组件测试一下:

// Roll.js
import React from 'react'
import {connect} from '../../store/connect'

const mapStateToProps = state => {  
    return {num: state.num}
}

const mapDispatchToProps = dispatch => {  
    return {addNum: () => {dispatch({type: 'add'})      
        },
        lowNum: () => {dispatch({type: 'low'})      
        }  
    }
}
class Roll extends React.Component {constructor(props) {super(props)
    }
    render() {
        return (<div style={{ textAlign:'center', paddingTop:'100px'}}>
                <button onClick={()=> this.props.lowNum()}>low</button>
                <div style={{display: 'inline', padding:'0 10px'}}>{this.props.num}</div>
                <button onClick={()=> this.props.addNum()}>add</button>
            </div>
        )
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(Roll)

最终后果:

redux 中间件(middleware)

大家可能都用过 redux 的一些中间件,比方 redux-thunk,redux-saga,redux-logger 等等,然而这些中间件是怎么实现的呢?咱们一一道来。

首先为什么须要中间件呢?假如当初有一个场景,咱们须要打印咱们每次 dispatch 的记录,那很简略能想到就是在执行 dispatch 后打印即可:

function dispatchAndPrint(store, dispatch) {dispatch({type: 'add'})
    console.log('newState:', store.getState())
}

然而当初又来了一个需要须要持续咱们捕捉 dispatch 时的谬误,那咱们须要怎么写呢:

function dispatchAndCatch(store, dispatch) {
    try {dispatch({type: 'add'})
    } catch(e) {console.error('dispatch error:', err)  
        throw e
    }
}

那如果当这些需要越来越多,咱们实际上也会写越来越多的 dispatch,实际上咱们能够把这一步 dispatch 提取进去:

let next = store.dispatch
store.dispatch = function dispatchAndPrint(store) {next({type: 'add'})
    console.log('newState:', store.getState())
}

store.dispatch = function dispatchAndCatch(store, dispatch) {
    try {next({type: 'add'})
    } catch(e) {console.error('dispatch error:', err)  
        throw e
    }
}

applyMiddleware

咱们在 redux 中应用中间件的时候,都会用到 applyMiddlewareapplyMiddleware 实际上和下面咱们写的例子的性能是差不多的,你能够了解成 applyMiddleware 先去获取一个 dispatch,而后在中间件中批改dispatch,具体 dispatch 会被革新成什么样取决于咱们的中间件。对此咱们能够实现一个简略版的applyMiddleware 函数。

const applyMiddleware = function(store, middleware){
  let next = store.dispatch;
  store.dispatch = middleware(store)(next);
}
applyMiddleware(dispatchAndPrint)

多个中间件的链式调用

过后实际上咱们应用 applyMiddleware 的时候必定不是说每次只能应用一个中间件,那如果应用多个中间件该怎么实现呢?

咱们能够将前一个中间件返回的 dispatch,作为下一个中间件的next 函数 传入,对此咱们能够将两个函数进行 柯里化

const dispatchAndPrint = store => next => action => {console.log('newState:', store.getState())
    return next(action)
}

const dispatchAndCatch = store => next => action => {
    try {next(action)
    } catch(e) {console.error('dispatch error:', err)  
        throw e
    }
}

编写 applyMiddleware:

function applyMiddleware(store, middlewares) {
    // 浅拷贝,避免前面 reverse 影响到原 middleware
    middlewares = middlewares.slice() 
    // 最后面放入的中间件应该在后面执行,此处若不翻转数组,最先放入的函数将会在最里层会导致最初才执行
    middlewares.reverse() 
    
    let dispatch = store.dispatch
    middlewares.map((middleware) => {dispatch = middleware(store)(dispatch)
    })
    return {...store, dispatch}
}

这边咱们解释一下 applyMiddleware 这个函数,实际上 middlewares 是一个中间件的数组,咱们对 middlewares 数组做反转解决是因为每次咱们的中间件函数只是返回了一个新的 dispatch 函数给下一个中间件,而咱们最终拿到的是最初这个包装 dispatch 的中间件返回的函数,若反转的话则最初这个中间件会先执行而后一直向前推能力执行到第一个中间件。

走进 applyMiddleware 源码

当然咱们看 applyMiddleware 的源码的话并不是像咱们一样间接反转中间件数组,而是上面这种写法:

function applyMiddleware(...middlewares) {return (createStore) => (reducer, preloadedState, enhancer) => {var store = createStore(reducer, preloadedState, enhancer);
    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}
  }
}

compose 函数 的实现:

function compose(...funcs) {if (funcs.length === 0) {return arg}

  if (funcs.length === 1) {
    // 只须要执行一个函数,把函数执行,把其后果返回即可
    return funcs[0]
  }
  // 多个函数执行时,利用 reduce 去递归解决这些函数
  return funcs.reduce((a, b) => (...args: any) => a(b(...args)))

咱们能够看到 applyMiddleware 的源码中实际上通过 compose 函数 去实现将上一个中间件的返回值传递下一个中间件作为参数,从而实现中间件串联的成果。

如果中间件程序是 a,b,ccompose 函数 组合后后果是c(b(a(...args))), 执行程序为a->b->c

总结

兴许前面你看到 redux-thunk 的源码的时候我可能会觉着这个库为什么这么简略就这么几行代码,然而其实没必要诧异,因为就算是 redux 也不是很简单,然而背地蕴含的 JS 编程思维却值得去学习,比方函数的 柯里化 函数式编程 装璜器 等等常识。

材料:

8k 字 | Redux/react-redux/redux 中间件设计实现分析

redux 中间件的原理

Redux 入门教程(二):中间件与异步操作

JavaScript 函数柯里化

代码组合(compose)

退出移动版