Rudex 与 React-Redux

这个章节我们旨在达到3个目的:

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

redux的实现

Q:为什么需要Redux? Redux帮助我们解决了什么问题?

A:React是一个组件化开发的UI库,组件之间存在着大量的通信,有的时候这个通信跨域多个组件,或者多个组件之间共享同一套数据,简单的父子组件之间传值不能满足我们的需求。自然而然的我们需要有一个地方存取和操作这些公共状态。而Redux就为我们提供了一种管理公共状态的方案。

Q: 如何管理公共状态?

A:既然是公共状态,那么我们把公共状态提取出来就好了。我们创建一个store.js文件,然后直接在里边存放公共的state,其他组件只要引入这个store就可以存取公共状态了。

const state = {
    count: 0
}

这是最简单的store设计,当然这样做存在两个比较大的缺点:

1. 容易误操作

比如有人不小心把store清空了,或者误修改了其他组件的数据,那显然不太安全,出错了也很难排查。因此我们需要有条件的操作store,防止直接修改store内部存取的数据。

2. 可读性比较差

js是一门极其依赖语义化的语言,试想如果在代码中不经注释直接修改了公共的state,在多人维护的场景下,为了搞清楚state的含义,还要翻看多处的代码,所以最好给每个操作都起个名字。

Q: 如何根据上述情况重新设计store?

A:根据上面的分析,我们希望公共状态既能被全局访问到,又是私有的不能被直接修改。这一点刚好可以使用闭包来实现。我们要存取状态,那么又涉及到gettersetter,当状态发生变化的时候,通知组件状态发生了变更。正好对应redux的三个API:getStatedispatchsubscribe

大致的store轮廓:

export const store = () =>{
    let initialState = {}
    function getState() {}
    function dispatch() {}
    function subscribe() {}
    return {
        getState,
        dispatch,
        subscribe
    }
}

Q: getState()函数的实现?

A: getState()实现非常简单,只需要返回store的当前状态值即可。

export const createStore = () =>{
    let currentState = {} // 公共状态
    function getState(){  // getter
        return currentState
    }
    function dispatch() {}
    function subscribe() {}
    return {
        getState,
        dispatch,
        subscribe
    }
}

Q: dispatch()函数的实现?

A: dispatch()函数的实现,我们的目标是有条件、具名的修改store的数据。在使用dispatch的时候,我们会给dispatch()传入一个action对象,这个对象包括我们要修改的state以及这个操作的名字actionType,根据type的不同,store会修改对应的state

export const createStore = () =>{
    let currentState = {}
    function getState(){
        return currentState
    }
    function dispatch(action){
        switch(action.type){
            case 'add':
                currentState = {
                    ...state,
                    count: currentState.count + 1
                }
                break;
            default:
                break;
        }
    }
    function subscribe() {}
    return {
        getState,
        dispatch,
        subscribe
    }
}

我们把对actionType的判断写在dispatch中显得臃肿,也很笨拙我们把修改state的规则抽离出来到一个reducer中。

Q: reducer()函数的实现?

A: reducer的目的是根据action对象的type来更新状态。

// store.js
import { reducer } from './reducer';
export const createStore = () =>{
    let currentState = {}
    function getState(){
        return currentState
    }
    function dispatch(action){
        currentState = reducer(currentState, action)
    }
    function subscribe() {}
    dispatch({ type: '@REDUX_INIT' });// 初始化store数据
    return {
        getState,
        dispatch,
        subscribe
    }
}

// reducer.js

const initialState = {
    count: 0
}

export function reducer(state = initialState, action){
    switch(action.type){
        case 'add':
            return {
                ...state,
                count:state.count+1
            }
        case 'substract':
            return {
                 ...state,
                count:state.count-1
            }
        default:
            return initialState
    }
}

Q: subscribe()函数的实现

A: 尽管我们已经能够存取公共的state,但state的变化并不会直接引起视图的更新,我们需要监听store的变化,这里我们应用一个设计模式————观察者模式,观察者模式被广泛应用于监听事件实现。

观察者模式的简单实现


class Observer {
    constructor(fn){
        this.update = fn
    }
}

class Subject{
    constructor(){
        this.observers = [];
    }
    addObserver(observer){
        this.observers.push(observer)
    }
    notify(){
        this.observers.forEach(observer=>observer.update())
    }
}

const subject = new Subject()
const update = () => console.log('被观察者发出通知')

const obj1 = new Observer(update)
const obj2 = new Observer(update)

subject.addObserver(obj1)
subject.addObserver(obj2)
subject.notify()

subscribe()函数的实现就类似上述,每次dispatch的时候都进行广播。

import { reducer } from './reducer';
export const createStore = () =>{
    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,
        dispatch,
        subscribe
    }
}

到这里一个简单的redux就已经完成了,但是我们在使用store的时候,需要在每个组件中引入store,然后getState,然后dispatch,还有subscribe,代码比较冗余,我们需要合并一些重复的操作,而其中一个解决方案就是react-redux

react-redux的实现

上文实现的redux在一个组件如果要从store中存取公共状态的时候需要进行4步操作:

  1. import 引入 store
  2. getState获取状态
  3. dispatch修改状态
  4. subscribe订阅更新

步骤繁琐,代码相对冗余。react-redux提供了Providerconnect两个API,Providerstore放入了this.context中,省去了import这个步骤,connectgetStatedispatch合并进了this.props,并自动订阅更新,简化了另外三个步骤。

Q: Provider的实现?

A: Provider是一个组件,接收一个store并将其放入全局的context对象中。

import React, { Component } from 'react';
import PropTypes from 'props-types';

export class Provider extends Component{
    // 需要声明静态属性childContextTypes来指定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
    }
}

完成Provider之后,我们就能在组件中通过this.context.store这样的形式获取到store,不需要再单独的import

Q: connect()函数的实现?

A: connect(mapStateToProps, mapDispatchToProps)(App)是一个高阶函数,接收一个组件然后返回一个新的组件。connect根据传入的mapstatedispatch方法挂载到组件的props上。

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(){
                // 触发更新
                this.forceUpdate()
                // 或者找出变化的状态然后setState进行更新
            }

            render(){
                <Component
                    {...this.props}
                    {...mapStateToProps(this.context.store.getState())}
                    {...mapDispatchToProps(this.context.store.dispatch)}
                />
            }
        }
        // 接收context的固定写法
        Connect.contextTypes = {
            store: PropsTypes.object
        }
        return Connect
    }
}

redux Middleware实现

所谓中间件,我们理解为拦截器,用于对某些过程进行拦截和处理,且中间件能够串行使用。在redux中,我们中间件拦截的是dispatch提交到reducer这个过程,从而增加dispatch的功能。

Q: 如何来实现一个每次dispatch之后手动打印store的中间件?

A: 封装一个公用的dispatch方法。

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

简单的替换确实可以达到上述的效果,但是假如我们需要其他的中间件,随着功能模块的增多,代码量会迅速膨胀,以后这个中间件就没法维护了,我们希望不同的功能是独立的可插拔的模块。

模块化

// 日志打印中间件
function patchStoreToAddLogger(store){
    let next = store.dispatch
    store.dispatch = function dispatchAndLog(action){
        console.log('prev state', store.getState())
        let result = next(action)
        console.log('next state', store.getState())
        return result
    }
}
// 错误监控中间件
function patchStoreToAddErrorCatch(store){
     let next = store.dispatch
     store.dispatch = function dispatchAndShowErrors(action){
         try{
             return next(action)
         }catch(err){
             console.err('捕获一个异常', err)
             throw err
         }
     }
}

patchStoreToAddLogger(store)
patchStoreToAddErrorCatch(store)

到这里我们基本实现了可组合,可插拔的中间件,但是代码可以进行优化。我们上述都是先获取dispatch然后在方法内部替换dispatch,这部分代码可以进行优化:我们不在方法内替换dispatch,而是返回一个新的dispatch,然后让循环来进行每一步的替换。

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

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

function applyMiddleware(store, middlewares){
    middlewares = [...middlewares] // 浅拷贝数组避免reverse改变原数组
    middlewares.reverse()
    // 循环替换dispatch
    middlewares.forEach(middleware=>store.dispatch = middleware(store))
}

这样的话我们就能使用下面的方式进行增加中间件了

applyMiddleware(store, [ logger, errCatch])

这个函数功能上已经满足了我们的需求但是看起来还是不够,函数在函数体内修改了store自身的dispatch,产生了所谓的副作用,我们可以把applyMiddleware作为高阶函数,用于增强store而不是替换dispatch

先对createStore进行一个小改造.

// store.js
export const createStore = (reducer, heightener)=> {
    // 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: '@@INIT'})
    return {
        getState,
        dispatch,
        subscribe
    }
}

// 中间件进一步柯里化
const looger = 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
    }
    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)))
}
上次更新时间: 2020-02-15 13:09:00