详解Redux、React-redux及Redux中间件

2年前 (2022) 程序员胖胖胖虎阿
312 0 0

Redux结构

详解Redux、React-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函数的效果图:

详解Redux、React-redux及Redux中间件
没加subscribe函数的效果图:
详解Redux、React-redux及Redux中间件
没加的话实际就是更新了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 了。
详解Redux、React-redux及Redux中间件

// 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、React-redux及Redux中间件

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)

版权声明:程序员胖胖胖虎阿 发表于 2022年11月6日 上午12:40。
转载请注明:详解Redux、React-redux及Redux中间件 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...