编程开源技术交流,分享技术与知识

网站首页 > 开源技术 正文

手把手深入Redux react-redux中间件设计及原理(下)【实践】

wxchong 2024-08-04 02:45:29 开源技术 49 ℃ 0 评论


作者:写代码像蔡徐坤

转发链接:https://mp.weixin.qq.com/s/hlwe0Z9Bhwyz2srHGJGigA

其实笔者本来没有redux相关的行文计划,不过公司内部最近有同事作了redux相关的技术分享,而笔者承担了一部分文章评审的任务,在评审的过程中,笔者花了相当的精力时间来查阅资料和实现代码,前后积攒了几千字的笔记,对redux也有了一份心得见解,于是顺手写就本文,希望能给大家带来些一些启发和思考Thanks?(?ω·)?经过本文的学习,读者应该能够学习理解:

?

  1. redux的设计思路及实现原理
  2. react-redux的设计思路及实现原理
  3. redux中间件的设计思路及实现原理

上一篇《手把手深入Redux react-redux中间件设计及原理(上)【实践】》讲解了redux的设计思路及实现原理和react-redux的设计思路及实现原理。

写完了react-redux,我们可以写个demo来测试一下:使用react-create-app创建一个项目,删掉无用的文件,并创建store.js、reducer.js、react-redux.js来分别写我们redux和react-redux的代码,index.js是项目的入口文件,在App.js中我们简单的写一个计数器,点击按钮就派发一个dispatch,让store中的count加一,页面上显示这个count。最后文件目录和代码如下:

// store.js
export const createStore = (reducer) => {    
    let currentState = {}    
    let observers = []             //观察者队列    
    function getState() {        
        return currentState    
    }    
    function dispatch(action) {        
        currentState = reducer(currentState, action)       
        observers.forEach(fn => fn())    
    }    
    function subscribe(fn) {        
        observers.push(fn)    
    }    
    dispatch({ type: '@@REDUX_INIT' }) //初始化store数据    
    return { getState, subscribe, dispatch }
}
//reducer.js
const initialState = {    
    count: 0
}

export function reducer(state = initialState, action) {    
    switch(action.type) {      
        case 'plus':        
        return {            
            ...state,            
            count: state.count + 1        
        }      
        case 'subtract':        
        return {            
            ...state,            
            count: state.count - 1        
        }      
        default:        
        return initialState    
    }
}
//react-redux.js
import React from 'react'
import PropTypes from 'prop-types'
export class Provider extends React.Component {  
    // 需要声明静态属性childContextTypes来指定context对象的属性,是context的固定写法  
    static childContextTypes = {    
        store: PropTypes.object  
    }  

    // 实现getChildContext方法,返回context对象,也是固定写法  
    getChildContext() {    
        return { store: this.store }  
    }  

    constructor(props, context) {    
        super(props, context)    
        this.store = props.store  
    }  

    // 渲染被Provider包裹的组件  
    render() {    
        return this.props.children  
    }
}

export function connect(mapStateToProps, mapDispatchToProps) {    
    return function(Component) {      
    class Connect extends React.Component {        
        componentDidMount() {          //从context获取store并订阅更新          
            this.context.store.subscribe(this.handleStoreChange.bind(this));        
        }        
        handleStoreChange() {          
            // 触发更新          
            // 触发的方法有多种,这里为了简洁起见,直接forceUpdate强制更新,读者也可以通过setState来触发子组件更新          
            this.forceUpdate()        
        }        
        render() {          
            return (            
                <Component              
                    // 传入该组件的props,需要由connect这个高阶组件原样传回原组件              
                    { ...this.props }              
                    // 根据mapStateToProps把state挂到this.props上              
                    { ...mapStateToProps(this.context.store.getState()) }               
                    // 根据mapDispatchToProps把dispatch(action)挂到this.props上              
                    { ...mapDispatchToProps(this.context.store.dispatch) }             
                />          
            )        
        }      
    }      

    //接收context的固定写法      
    Connect.contextTypes = {        
        store: PropTypes.object      
    }      
    return Connect    
    }
}  
//index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import { Provider } from './react-redux'
import { createStore } from './store'
import { reducer } from './reducer'

ReactDOM.render(   
    <Provider store={createStore(reducer)}>        
        <App />    
    </Provider>,     
    document.getElementById('root')
);
//App.js
import React from 'react'
import { connect } from './react-redux'

const addCountAction = {  
    type: 'plus'
}

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

const mapDispatchToProps = dispatch => {  
    return {      
        addCount: () => {          
            dispatch(addCountAction)      
        }  
    }
}

class App extends React.Component {  
    render() {    
        return (      
            <div className="App">        
                { this.props.count }        
                <button onClick={ () => this.props.addCount() }>增加</button>      
            </div>    
        );  
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(App)

运行项目,点击增加按钮,能够正确的计数,OK大成功,我们整个redux、react-redux的流程就走通了

三. redux Middleware实现

上面redux和react-redux的实现都比较简单,下面我们来分析实现稍困难一些的「redux中间件」。所谓中间件,我们可以理解为拦截器,用于对某些过程进行拦截和处理,且中间件之间能够串联使用。在redux中,我们中间件拦截的是dispatch提交到reducer这个过程,从而增强dispatch的功能。

我查阅了很多redux中间件相关的资料,但最后发现没有一篇写的比官方文档清晰,文档从中间件的需求到设计,从概念到实现,每一步都有清晰生动的讲解。下面我们就和文档一样,以一个记录日志的中间件为例,一步一步分析redux中间件的设计实现。

我们思考一下,如果我们想在每次dispatch之后,打印一下store的内容,我们会如何实现呢:

1. 在每次dispatch之后手动打印store的内容

store.dispatch({ type: 'plus' })
console.log('next state', store.getState())

这是最直接的方法,当然我们不可能在项目里每个dispatch后面都粘贴一段打印日志的代码,我们至少要把这部分功能提取出来。

2. 封装dispatch

function dispatchAndLog(store, action) {    
    store.dispatch(action)    
    console.log('next state', store.getState())
}

我们可以重新封装一个公用的新的dispatch方法,这样可以减少一部分重复的代码。不过每次使用这个新的dispatch都得从外部引一下,还是比较麻烦。

3. 替换dispatch

let next = store.dispatch
store.dispatch = function dispatchAndLog(action) {  
    let result = next(action)  
    console.log('next state', store.getState())  
    return result
}

如果我们直接把dispatch给替换,这样每次使用的时候不就不需要再从外部引用一次了吗?对于单纯打印日志来说,这样就足够了,但是如果我们还有一个监控dispatch错误的需求呢,我们固然可以在打印日志的代码后面加上捕获错误的代码,但随着功能模块的增多,代码量会迅速膨胀,以后这个中间件就没法维护了,我们希望不同的功能是「独立的可拔插的」模块。

4. 模块化

// 打印日志中间件
function patchStoreToAddLogging(store) {    
    let next = store.dispatch    //此处也可以写成匿名函数    
    store.dispatch = function dispatchAndLog(action) {      
        let result = next(action)      
        console.log('next state', store.getState())      
        return result    
    }
}  

// 监控错误中间件
function patchStoreToAddCrashReporting(store) {    
    //这里取到的dispatch已经是被上一个中间件包装过的dispatch, 从而实现中间件串联    
    let next = store.dispatch    
    store.dispatch = function dispatchAndReportErrors(action) {        
        try {            
            return next(action)        
        } catch (err) {            
            console.error('捕获一个异常!', err)            
            throw err        
        }    
    }
}

我们把不同功能的模块拆分成不同的方法,通过在方法内「获取上一个中间件包装过的store.dispatch实现链式调用」。然后我们就能通过调用这些中间件方法,分别使用、组合这些中间件。

patchStoreToAddLogging(store)
patchStoreToAddCrashReporting(store)

到这里我们基本实现了可组合、拔插的中间件,但我们仍然可以把代码再写好看一点。我们注意到,我们当前写的中间件方法都是先获取dispatch,然后在方法内替换dispatch,这部分重复代码我们可以再稍微简化一下:我们不在方法内替换dispatch,而是返回一个新的dispatch,然后让循环来进行每一步的替换。

5. applyMiddleware

改造一下中间件,使其返回新的dispatch而不是替换原dispatch

function logger(store) {    
    let next = store.dispatch     
 
    // 我们之前的做法(在方法内直接替换dispatch):    
    // store.dispatch = function dispatchAndLog(action) {    
    //         ...    
    // }    
  
    return function dispatchAndLog(action) {        
        let result = next(action)        
        console.log('next state', store.getState())        
        return result    
    }
}

在Redux中增加一个辅助方法applyMiddleware ,用于添加中间件

function applyMiddleware(store, middlewares) {    
    middlewares = [ ...middlewares ]    //浅拷贝数组, 避免下面reserve()影响原数组    
    middlewares.reverse()               //由于循环替换dispatch时,前面的中间件在最里层,因此需要翻转数组才能保证中间件的调用顺序      
    // 循环替换dispatch   
    middlewares.forEach(middleware =>      
        store.dispatch = middleware(store)    
    )
}

然后我们就能以这种形式增加中间件了:

applyMiddleware(store, [ logger, crashReporter ])

写到这里,我们可以简单地测试一下中间件。我创建了三个中间件,分别是logger1、thunk、logger2,其作用也很简单,打印logger1 -> 执行异步dispatch -> 打印logger2,我们通过这个例子观察中间件的执行顺序

//index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { Provider } from './react-redux'
import { createStore } from './store'
import { reducer } from './reducer'

let store = createStore(reducer)

function logger(store) {    
    let next = store.dispatch    
    return (action) => {        
        console.log('logger1')        
        let result = next(action)        
        return result    
    }
}

function thunk(store) {    
    let next = store.dispatch    
    return (action) => {        
        console.log('thunk')        
        return typeof action === 'function' ? action(store.dispatch) : next(action)    
    }
}

function logger2(store) {    
    let next = store.dispatch        
    return (action) => {        
        console.log('logger2')        
        let result = next(action)        
        return result    
    }
}

function applyMiddleware(store, middlewares) {    
    middlewares = [ ...middlewares ]      
    middlewares.reverse()     
    middlewares.forEach(middleware =>      
        store.dispatch = middleware(store)    
    )
}

applyMiddleware(store, [ logger, thunk, logger2 ])

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

发出异步dispatch

function addCountAction(dispatch) {  
    setTimeout(() => {    
        dispatch({ type: 'plus' })  
    }, 1000)
}

dispatch(addCountAction)

输出结果

可以看到,控制台先输出了中间件logger1的打印结果,然后进入thunk中间件打印了'thunk',等待一秒后,异步dispatch被触发,又重新走了一遍logger1 -> thunk -> logger2。到这里,我们就基本实现了可拔插、可组合的中间件机制,还顺便实现了redux-thunk。

6. 纯函数

之前的例子已经基本实现我们的需求,但我们还可以进一步改进,上面这个函数看起来仍然不够"纯",函数在函数体内修改了store自身的dispatch,产生了所谓的"副作用",从函数式编程的规范出发,我们可以进行一些改造,借鉴react-redux的实现思路,我们可以把applyMiddleware作为高阶函数,用于增强store,而不是替换dispatch:

先对createStore进行一个小改造,传入heightener(即applyMiddleware),heightener接收并强化createStore。

// store.js
export const createStore = (reducer, heightener) => {    
    // heightener是一个高阶函数,用于增强createStore    
    //如果存在heightener,则执行增强后的createStore    
    if (heightener) {        
        return heightener(createStore)(reducer)    
    }        
    let currentState = {}    
    let observers = []             //观察者队列    
    function getState() {        
        return currentState    
    }    
    function dispatch(action) {        
        currentState = reducer(currentState, action);        
        observers.forEach(fn => fn())    
    }    
    function subscribe(fn) {        
        observers.push(fn)    
    }    
    dispatch({ type: '@@REDUX_INIT' })//初始化store数据    
    return { getState, subscribe, dispatch }
}

中间件进一步柯里化,让next通过参数传入

const logger = store => next => action => {    
    console.log('log1')    
    let result = next(action)    
    return result
}

const thunk = store => next =>action => {
    console.log('thunk')    
    const { dispatch, getState } = store    
    return typeof action === 'function' ? action(store.dispatch) : next(action)
}

const logger2 = store => next => action => {    
    console.log('log2')    
    let result = next(action)    
    return result
}

改造applyMiddleware

const applyMiddleware = (...middlewares) => createStore => reducer => {    
    const store = createStore(reducer)    
    let { getState, dispatch } = store    
    const params = {      
        getState,      
        dispatch: (action) => dispatch(action)      
        //解释一下这里为什么不直接 dispatch: dispatch      
        //因为直接使用dispatch会产生闭包,导致所有中间件都共享同一个dispatch,如果有中间件修改了dispatch或者进行异步dispatch就可能出错    
    }    

    const middlewareArr = middlewares.map(middleware => middleware(params)) 
   
    dispatch = compose(...middlewareArr)(dispatch)    
    return { ...store, dispatch }
}

//compose这一步对应了middlewares.reverse(),是函数式编程一种常见的组合方法
function compose(...fns) {
    if (fns.length === 0) return arg => arg    
    if (fns.length === 1) return fns[0]    
    return fns.reduce((res, cur) =>(...args) => res(cur(...args)))
}

代码应该不难看懂,在上一个例子的基础上,我们主要做了两个改造

  1. 使用compose方法取代了middlewares.reverse(),compose是函数式编程中常用的一种组合函数的方式,compose内部使用reduce巧妙地组合了中间件函数,使传入的中间件函数变成 (...arg) => mid3(mid1(mid2(...arg)))这种形式
  2. 不直接替换dispatch,而是作为高阶函数增强createStore,最后return的是一个新的store

7.洋葱圈模型

之所以把洋葱圈模型放到后面来讲,是因为洋葱圈和前边中间件的实现并没有很紧密的关系,为了避免读者混淆,放到这里提一下。我们直接放出三个打印日志的中间件,观察输出结果,就能很轻易地看懂洋葱圈模型。

const logger1 = store => next => action => {    
    console.log('进入log1')    
    let result = next(action)    
    console.log('离开log1')    
    return result
}

const logger2 = store => next => action => {    
    console.log('进入log2')    
    let result = next(action)    
    console.log('离开log2')    
    return result
}

const logger3 = store => next => action => {    
    console.log('进入log3')    
    let result = next(action)    
    console.log('离开log3')    
    return result
}

执行结果

由于我们的中间件是这样的结构:

logger1(    
    console.log('进入logger1')    
        logger2(        
            console.log('进入logger2')        
                logger3(            
                    console.log('进入logger3')            
                    //dispatch()            
                    console.log('离开logger3')        
                )        
            console.log('离开logger2')    
        )    
    console.log('离开logger1')
)

因此我们可以看到,中间件的执行顺序实际上是这样的:

进入log1 -> 执行next -> 进入log2 -> 执行next -> 进入log3 -> 执行next -> next执行完毕 -> 离开log3 -> 回到上一层中间件,执行上层中间件next之后的语句 -> 离开log2 -> 回到中间件log1, 执行log1的next之后的语句 -> 离开log1

?

这就是所谓的"洋葱圈模型"

?

四. 总结 & 致谢

其实全文看下来,读者应该能够体会到,redux、react-redux以及redux中间件的实现并不复杂,各自的核心代码不过十余行,但在这寥寥数行代码之间,蕴含了一系列编程思想与设计范式 —— 观察者模式、装饰器模式、中间件原理、函数柯里化、函数式编程。我们阅读源码的意义,也就在于理解和体会这些思想。

全篇成文前后经历一个月,主要参考资料来自同事分享以及多篇相关文章,在此特别感谢龙超大佬和于中大佬的分享。在考据细节的过程中,也得到了很多素未谋面的朋友们的解惑,特别是感谢Frank1e大佬在中间件柯里化理解上给予的帮助。真是感谢大家Thanks?(?ω·)?

作者:写代码像蔡徐坤

转发链接:https://mp.weixin.qq.com/s/hlwe0Z9Bhwyz2srHGJGigA

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表