一 前言

状态管理是单页面应用解决组件状态共享,复杂组件通信的技术方案。接下来的两个章节,我们将详细介绍 React 应用中常见的两种状态管理方式- React-ReduxReact-Mobx
本章节主要讲 React-Redux,包括Redux 设计思想、中间件原理,以及 React-Redux 的用法和原理。

1 状态管理应用场景

状态管理工具为什么受到开发者的欢迎呢?我认为首先应该想想状态管理适用于什么场景。解决了什么问题。
① 组件之间共用数据,如何处理?
设想一种场景,就是一些通过 ajax 向服务器请求的重要数据,比如用户信息,权限列表,可能会被多个组件需要,那么如果每个组件初始化都请求一遍数据显然是不合理的。这时候常用的一种解决方案是,应用初始化时候,只请求一次数据,然后通过状态管理把数据存起来,需要数据的组件只需要从状态管理中‘拿’就可以了。
效果图:4.react-redux - 图1
② 复杂组件之间如何通信?
还有一种场景就是对于 spa 单页面应用一切皆组件,对于嵌套比较深的组件,组件通信成了一个棘手的问题。比如如下的场景, B 组件向 H 组件传递某些信息,那么常规的通信方式似乎难以实现。
这个时候状态管理就派上用场了,可以把 B 组件的信息传递给状态管理层,H 组件连接状态管理层,再由状态管理层通知 H 组件,这样就本质解决了组件通信问题。
4.react-redux - 图2

2 React-Redux,Redux,React三者关系

在深入研究 React-Redux 之前,应该先弄明白 React-Redux ,Redux , React 三者到底是什么关系。

  • Redux: 首先 Redux 是一个应用状态管理js库,它本身和 React 是没有关系的,换句话说,Redux 可以应用于其他框架构建的前端应用,甚至也可以应用于 Vue 中。
  • React-Redux:React-Redux 是连接 React 应用和 Redux 状态管理的桥梁。React-redux 主要专注两件事,一是如何向 React 应用中注入 redux 中的 Store ,二是如何根据 Store 的改变,把消息派发给应用中需要状态的每一个组件。
  • React:这个就不必多说了。

三者的关系图如下所示:
4.react-redux - 图3

3 温习 Redux

彻底弄明白 React-Redux 之前,就必须要搞懂 Redux 在 React 中扮演的角色。Redux 的设计满足以下三个原则:

①三大原则

  • 1 单向数据流:整个 redux ,数据流向都是单向的,我用一张官网的图片描述整个数据流动的流程。

4.react-redux - 图4

  • 2 state 只读:在 Redux 中不能通过直接改变 state ,来让状态发生变化,如果想要改变 state ,那就必须触发一次 action ,通过 action 执行每个 reducer 。
  • 3 纯函数执行:每一个 reducer 都是一个纯函数,里面不要执行任何副作用,返回的值作为新的 state ,state 改变会触发 store 中的 subscribe 。

    ②发布订阅思想

    redux 可以作为发布订阅模式的一个具体实现。redux 都会创建一个 store ,里面保存了状态信息,改变 store 的方法 dispatch ,以及订阅 store 变化的方法 subscribe 。

    ③中间件思想

    redux 应用了前端领域为数不多的中间件 compose ,那么 redux 的中间件是用来做什么的? 答案只有一个: 那就是强化 dispatch , Redux 提供了中间件机制,使用者可以根据需要来强化 dispatch 函数,传统的 dispatch 是不支持异步的,但是可以针对 Redux 做强化,于是有了 redux-thunk,redux-actions ,redux-saga等中间件,包括 dvajs 中,也写了一个 redux 支持 promise 的中间件。
    一起来看一下 compose 是如何实现的:

    1. const compose = (...funcs) => {
    2. return funcs.reduce((f, g) => (x) => f(g(x)));
    3. }
  • funcs 为中间件组成的数组,compose 通过数组的 reduce 方法,实现执行每一个中间件,强化 dispatch 。

    ④核心api

    对于内部原理,我这里就不多说了,毕竟这节主要讲的是 React-Redux ,主要先来看一下 redux 几个比较核心的 api:
    createStore
    createStore redux中通过 createStore 可以创建一个 Store ,使用者可以将这个 Store 保存传递给 React 应用,具体怎么传递那就是 React-Redux 做的事了。首先看一下 createStore 的使用:

    const Store = createStore(rootReducer,initialState,middleware)

  • 参数一 reducers : redux 的 reducer ,如果有多个那么可以调用 combineReducers 合并。

  • 参数二 initialState :初始化的 state 。
  • 参数三 middleware :如果有中间件,那么存放 redux 中间件。

combineReducers

  1. /* 将 number 和 PersonalInfo 两个reducer合并 */
  2. const rootReducer = combineReducers({ number:numberReducer,info:InfoReducer })
  • 正常状态可以会有多个 reducer ,combineReducers 可以合并多个reducer。

applyMiddleware

const middleware = applyMiddleware(logMiddleware)

  • applyMiddleware 用于注册中间价,支持多个参数,每一个参数都是一个中间件。每次触发 action ,中间件依次执行。

    ⑤ 实战-redux基本用法

    第一步:编写reducer

    1. /* number Reducer */
    2. function numberReducer(state=1,action){
    3. switch (action.type){
    4. case 'ADD':
    5. return state + 1
    6. case 'DEL':
    7. return state - 1
    8. default:
    9. return state
    10. }
    11. }
    12. /* 用户信息reducer */
    13. function InfoReducer(state={},action){
    14. const { payload = {} } = action
    15. switch (action.type){
    16. case 'SET':
    17. return {
    18. ...state,
    19. ...payload
    20. }
    21. default:
    22. return state
    23. }
    24. }
  • 编写了两个 reducer ,一个管理变量 number ,一个保存信息 info 。

第二步:注册中间件

  1. /* 打印中间件 */
  2. /* 第一层在 compose 中被执行 */
  3. function logMiddleware(){
  4. /* 第二层在reduce中被执行 */
  5. return (next) => {
  6. /* 返回增强后的dispatch */
  7. return (action)=>{
  8. const { type } = action
  9. console.log('发生一次action:', type )
  10. return next(action)
  11. }
  12. }
  13. }
  • 在重点看一下 redux 的中间件的编写方式,本质上应用了函数柯里化。

第三步:生成Store

  1. /* 注册中间件 */
  2. const rootMiddleware = applyMiddleware(logMiddleware)
  3. /* 注册reducer */
  4. const rootReducer = combineReducers({ number:numberReducer,info:InfoReducer })
  5. /* 合成Store */
  6. const Store = createStore(rootReducer,{ number:1 , info:{ name:null } } ,rootMiddleware)
  • 这一步没什么好说的,直接注册就可以了。

第四步:试用redux

function Index(){
  const [ state , changeState  ] = useState(Store.getState())
  useEffect(()=>{
    /* 订阅state */
    const unSubscribe = Store.subscribe(()=>{
         changeState(Store.getState())
     })
    /* 解除订阅 */
     return () => unSubscribe()
  },[])
  return <div > 
          <p>  { state.info.name ? `hello, my name is ${ state.info.name}` : 'what is your name' } ,
           { state.info.mes ? state.info.mes  : ' what do you say? '  } </p>
         《React进阶实践指南》 { state.number } 👍 <br/>
        <button onClick={()=>{ Store.dispatch({ type:'ADD' })  }} >点赞</button>
        <button onClick={()=>{ Store.dispatch({ type:'SET',payload:{ name:'alien' , mes:'let us learn React!'  } }) }} >修改标题</button>
     </div>
}
  • 为了让大家直观看到效果,可以直接把 redux 和 react 直接结合起来使用,在 useEffect 中进行订阅和解除订阅,通过 useState 改变视图层。
  • store.getState 可以获取 redux 最新的 state 。

效果4.react-redux - 图5总结:
上述demo中,没有用到 react-redux ,但是明显暴露了很多问题。我来做一下总结:

  • 1 首先想要的状态是共用的,上述 demo 无法满足状态共用的情况。
  • 2 正常情况不可能将每一个需要状态的组件都用 subscribe / unSubscribe 来进行订阅
  • 3 比如 A 组件需要状态 a,B 组件需要状态 b ,那么改变 a,只希望 A 组件更新,不希望 B 组件更新,显然上述是不能满足的。
  • 4 …

所以为了解决上述诸多问题,react-redux 就应运而生了。

二 React-Redux用法

上述讲到 React-Redux 是沟通 React 和 Redux 的桥梁,它主要功能体现在如下两个方面:

  • 1 接受 Redux 的 Store,并把它合理分配到所需要的组件中。
  • 2 订阅 Store 中 state 的改变,促使消费对应的 state 的组件更新。

    1 用法简介

    Provider

    由于 redux 数据层,可能被很多组件消费,所以 react-redux 中提供了一个 Provider 组件,可以全局注入 redux 中的 store ,所以使用者需要把 Provider 注册到根部组件中。

  • Provider 作用就是保存 redux 中的 store ,分配给所有需要 state 的子孙组件。

例子🌰:

export default function Root(){
  return <Provider store={Store} >
      <Index />
  </Provider>
}

connect

既然已经全局注入了 Store ,那么需要 Store 中的状态或者想要改变Store的状态,那么如何处理呢,React-Redux 提供了一个高阶组件connect,被 connect 包装后组件将获得如下功能:

  • 1 能够从 props 中获取改变 state 的方法 Store.dispatch 。
  • 2 如果 connect 有第一个参数,那么会将 redux state 中的数据,映射到当前组件的 props 中,子组件可以使用消费。
  • 3 当需要的 state ,有变化的时候,会通知当前组件更新,重新渲染视图。

开发者可以利用 connect 提供的功能,做数据获取,数据通信,状态派发等操作。首先来看看 connect 用法。

function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)

①mapStateToProps

const mapStateToProps = state => ({ number: state.number })

  • 组件依赖 redux 的 state,映射到业务组件的 props 中,state 改变触发,业务组件 props 改变,触发业务组件更新视图。当这个参数没有的时候,当前组件不会订阅 store 的改变。

②mapDispatchToProps

const mapDispatchToProps = dispatch => {
  return {
    numberAdd: () => dispatch({ type: 'ADD' }),
    setInfo: () => dispatch({ type: 'SET' }),
  }
}
  • 将 redux 中的 dispatch 方法,映射到业务组件的 props 中。比如将如上 demo 中的两个方法映射到 props ,变成了 numberAdd , setInfo 方法。

③mergeProps

/*
* stateProps , state 映射到 props 中的内容
* dispatchProps, dispatch 映射到 props 中的内容。
* ownProps 组件本身的 props
*/
(stateProps, dispatchProps, ownProps) => Object

正常情况下,如果没有这个参数,会按照如下方式进行合并,返回的可以是对象,可以自定义的合并规则,还可以附加一些属性。
{ …ownProps, …stateProps, …dispatchProps }
④options

{
  context?: Object,   // 自定义上下文
  pure?: boolean, // 默认为 true , 当为 true 的时候 ,除了 mapStateToProps 和 props ,其他输入或者state 改变,均不会更新组件。
  areStatesEqual?: Function, // 当pure true , 比较引进store 中state值 是否和之前相等。 (next: Object, prev: Object) => boolean
  areOwnPropsEqual?: Function, // 当pure true , 比较 props 值, 是否和之前相等。 (next: Object, prev: Object) => boolean
  areStatePropsEqual?: Function, // 当pure true , 比较 mapStateToProps 后的值 是否和之前相等。  (next: Object, prev: Object) => boolean
  areMergedPropsEqual?: Function, // 当 pure 为 true 时, 比较 经过 mergeProps 合并后的值 , 是否与之前等  (next: Object, prev: Object) => boolean
  forwardRef?: boolean, //当为true 时候,可以通过ref 获取被connect包裹的组件实例。
}

如上标注了 options 属性每一个的含义。并且讲解了 react-redux 的基本用法,接下来简单实现 react-redux 的两个功能。

2 实践一:React-Redux实现状态共享

export default function Root(){
  React.useEffect(()=>{
    Store.dispatch({ type:'ADD'})
    Store.dispatch({ type:'SET',payload:{ name:'alien' , mes:'let us learn React!'  } })
  },[])
  return <Provider store={Store} >
      <Index />
  </Provider>
}
  • 通过在根组件中注入 store ,并在 useEffect 中改变 state 内容。

然后在整个应用中在想要获取数据的组件里,获取 state 中的内容。

import { connect } from 'react-redux'


class Index extends React.Component {
    componentDidMount() { }
    render() {
         const { info , number }:any = this.props  
        return <div >
            <p>  {info.name ? `hello, my name is ${info.name}` : 'what is your name'} ,
          {info.mes ? info.mes : ' what do you say? '} </p>
        《React进阶实践指南》 {number} 👍 <br />
        </div>
    }
}

const mapStateToProps = state => ({ number: state.number, info: state.info })

export default connect(mapStateToProps)(Index)
  • 通过 mapStateToProps 获取指定 state 中的内容,然后渲染视图。

效果:4.react-redux - 图6

3 实践二:React-Redux实现组件通信

接下来可以用 React-Redux 模拟一个,组件通信的场景。
组件A

function ComponentA({ toCompB, compBsay }) { /* 组件A */
  const [CompAsay, setCompAsay] = useState('')
  return <div className="box" >
    <p>我是组件A</p>
    <div> B组件对我说:{compBsay} </div>
        我对B组件说:<input placeholder="CompAsay" onChange={(e) => setCompAsay(e.target.value)} />
    <button onClick={() => toCompB(CompAsay)} >确定</button>
  </div>
}
/* 映射state中CompBsay  */
const CompAMapStateToProps = state => ({ compBsay: state.info.compBsay })
/* 映射toCompB方法到props中 */
const CompAmapDispatchToProps = dispatch => ({ toCompB: (mes) => dispatch({ type: 'SET', payload: { compAsay: mes } }) })
/* connect包装组件A */
export const CompA = connect(CompAMapStateToProps, CompAmapDispatchToProps)(ComponentA)
  • 组件 A 通过 mapStateToProps,mapDispatchToProps,分别将state 中的 compBsay 属性,和改变 state 的 compAsay 方法,映射到 props 中。

组件B

class ComponentB extends React.Component { /* B组件 */
  state={ compBsay:'' }
  handleToA=()=>{
     this.props.dispatch({ type: 'SET', payload: { compBsay: this.state.compBsay } })
  }
  render() {
    return <div className="box" >
      <p>我是组件B</p>
      <div> A组件对我说:{ this.props.compAsay } </div>
       我对A组件说:<input placeholder="CompBsay" onChange={(e)=> this.setState({ compBsay: e.target.value  }) }  />
      <button  onClick={ this.handleToA } >确定</button>
    </div>
  }
}
/* 映射state中 CompAsay  */
const CompBMapStateToProps = state => ({ compAsay: state.info.compAsay })
export const CompB =  connect(CompBMapStateToProps)(ComponentB)
  • B 组件和 A 组件差不多,通过触发 dispatch 向组件 A 传递信息,同时接受 B 组件的信息。

效果:4.react-redux - 图7

三 React-Redux原理

对于 React-Redux 原理,我按照功能组成,大致分为三部分,接下来将按照这三部分逐一击破:
对于 React-Redux 原理,我按照功能组成,大致分为三部分,接下来将按照这三部分逐一击破:

第一部分: Provider注入Store

react-redux/src/components/Provider.js

const ReactReduxContext =  React.createContext(null)
function Provider({ store, context, children }) {
   /* 利用useMemo,跟据store变化创建出一个contextValue 包含一个根元素订阅器和当前store  */ 
  const contextValue = useMemo(() => {
      /* 创建了一个根级 Subscription 订阅器 */
    const subscription = new Subscription(store)
    return {
      store,
      subscription
    } /* store 改变创建新的contextValue */
  }, [store])
  useEffect(() => {
    const { subscription } = contextValue
    /* 触发trySubscribe方法执行,创建listens */
    subscription.trySubscribe() // 发起订阅
    return () => {
      subscription.tryUnsubscribe()  // 卸载订阅
    } 
  }, [contextValue])  /*  contextValue state 改变出发新的 effect */
  const Context = ReactReduxContext
  return <Context.Provider value={contextValue}>{children}</Context.Provider>

这里保留了核心的代码。从这段代码,从中可以分析出 Provider 做了哪些事。

  • 1 首先知道 React-Redux 是通过 context 上下文来保存传递 Store 的,但是上下文 value 保存的除了 Store 还有 subscription 。
  • 2 subscription 可以理解为订阅器,在 React-redux 中一方面用来订阅来自 state 变化,另一方面通知对应的组件更新。在 Provider 中的订阅器 subscription 为根订阅器,
  • 3 在 Provider 的 useEffect 中,进行真正的绑定订阅功能,其原理内部调用了 store.subscribe ,只有根订阅器才会触发store.subscribe,至于为什么,马上就会讲到。

    第二部分: Subscription订阅器

    react-redux/src/utils/Subscription.js

    /* 发布订阅者模式 */
    export default class Subscription {
    constructor(store, parentSub) {
    //....
    }
    /* 负责检测是否该组件订阅,然后添加订阅者也就是listener */
    addNestedSub(listener) {
      this.trySubscribe()
      return this.listeners.subscribe(listener)
    }
    /* 向listeners发布通知 */
    notifyNestedSubs() {
      this.listeners.notify()
    }
    /* 开启订阅模式 首先判断当前订阅器有没有父级订阅器 , 如果有父级订阅器(就是父级Subscription),把自己的handleChangeWrapper放入到监听者链表中 */
    trySubscribe() {
      /*
      parentSub  即是provide value 里面的 Subscription 这里可以理解为 父级元素的 Subscription
      */
      if (!this.unsubscribe) {
        this.unsubscribe = this.parentSub
          ? this.parentSub.addNestedSub(this.handleChangeWrapper)
          /* provider的Subscription是不存在parentSub,所以此时trySubscribe 就会调用 store.subscribe   */
          : this.store.subscribe(this.handleChangeWrapper)
        this.listeners = createListenerCollection()
      }
    }
    /* 取消订阅 */
    tryUnsubscribe() {
       //....
    }
    }
    

    整个订阅器的核心,我浓缩提炼成8个字:层层订阅,上订下发
    层层订阅:React-Redux 采用了层层订阅的思想,上述内容讲到 Provider 里面有一个 Subscription ,提前透露一下,每一个用 connect 包装的组件,内部也有一个 Subscription ,而且这些订阅器一层层建立起关联,Provider中的订阅器是最根部的订阅器,可以通过 trySubscribe 和 addNestedSub 方法可以看到。还有一个注意的点就是,如果父组件是一个 connect ,子孙组件也有 connect ,那么父子 connect 的 Subscription 也会建立起父子关系。
    上订下发:在调用 trySubscribe 的时候,能够看到订阅器会和上一级的订阅器通过 addNestedSub 建立起关联,当 store 中 state 发生改变,会触发 store.subscribe ,但是只会通知给 Provider 中的根Subscription,根 Subscription 也不会直接派发更新,而是会下发给子代订阅器( connect 中的 Subscription ),再由子代订阅器,决定是否更新组件,层层下发。
    |————问与答————|
    问:为什么 React-Redux 会采用 subscription 订阅器进行订阅,而不是直接采用 store.subscribe 呢 ?

  • 1 首先 state 的改变,Provider 是不能直接下发更新的,如果下发更新,那么这个更新是整个应用层级上的,还有一点,如果需要 state 的组件,做一些性能优化的策略,那么该更新的组件不会被更新,不该更新的组件反而会更新了。

  • 2 父 Subscription -> 子 Subscription 这种模式,可以逐层管理 connect 的状态派发,不会因为 state 的改变而导致更新的混乱。

|————END————|

层层订阅模型:
4.react-redux - 图8

第三部分: connect控制更新

由于connect中的代码过于复杂,我这里只保留核心的流程,而且对代码进行简化处理。
react-redux/src/components/connectAdvanced.js

function connect(mapStateToProps,mapDispatchToProps){
    const Context = ReactReduxContext
    /* WrappedComponent 为connect 包裹的组件本身  */   
    return function wrapWithConnect(WrappedComponent){
        function createChildSelector(store) {
          /* 选择器  合并函数 mergeprops */
          return selectorFactory(store.dispatch, { mapStateToProps,mapDispatchToProps })
        }
        /* 负责更新组件的容器 */
        function ConnectFunction(props){
          /* 获取 context内容 里面含有 redux中store 和父级subscription */
          const contextValue = useContext(ContextToUse)
          /* 创建子选择器,用于提取state中的状态和dispatch映射,合并到props中 */
          const childPropsSelector = createChildSelector(contextValue.store)
          const [subscription, notifyNestedSubs] = useMemo(() => {
            /* 创建一个子代Subscription,并和父级subscription建立起关系 */
            const subscription = new Subscription(
              store,
              didStoreComeFromProps ? null : contextValue.subscription // 父级subscription,通过这个和父级订阅器建立起关联。
            )
             return [subscription, subscription.notifyNestedSubs]
            }, [store, didStoreComeFromProps, contextValue])

            /* 合成的真正的props */
            const actualChildProps = childPropsSelector(store.getState(), wrapperProps)
            const lastChildProps = useRef()
            /* 更新函数 */
            const [ forceUpdate, ] = useState(0)
            useEffect(()=>{
                const checkForUpdates =()=>{
                   newChildProps = childPropsSelector()
                  if (newChildProps === lastChildProps.current) { 
                      /* 订阅的state没有发生变化,那么该组件不需要更新,通知子代订阅器 */
                      notifyNestedSubs() 
                  }else{
                     /* 这个才是真正的触发组件更新的函数 */
                     forceUpdate(state=>state+1)
                     lastChildProps.current = newChildProps /* 保存上一次的props */
                  }
                }
                subscription.onStateChange = checkForUpdates
                //开启订阅者 ,当前是被connect 包转的情况 会把 当前的 checkForceUpdate 放在存入 父元素的addNestedSub中 ,一点点向上级传递 最后传到 provide 
                subscription.trySubscribe()
                /* 先检查一遍,反正初始化state就变了 */
                checkForUpdates()
            },[store, subscription, childPropsSelector])

             /* 利用 Provider 特性逐层传递新的 subscription */
            return  <ContextToUse.Provider value={{  ...contextValue, subscription}}>
                 <WrappedComponent  {...actualChildProps}  />
            </ContextToUse.Provider>  
          }
          /* memo 优化处理 */
          const Connect = React.memo(ConnectFunction) 
        return hoistStatics(Connect, WrappedComponent)  /* 继承静态属性 */
    }
}

connect 的逻辑还是比较复杂的,我总结一下核心流程。

  • 1 connect 中有一个 selector 的概念,selector 有什么用?就是通过 mapStateToProps ,mapDispatchToProps ,把 redux 中 state 状态合并到 props 中,得到最新的 props 。
  • 2 上述讲到过,每一个 connect 都会产生一个新的 Subscription ,和父级订阅器建立起关联,这样父级会触发子代的 Subscription 来实现逐层的状态派发。
  • 3 有一点很重要,就是 Subscription 通知的是 checkForUpdates 函数,checkForUpdates 会形成新的 props ,与之前缓存的 props 进行浅比较,如果不想等,那么说明 state 已经变化了,直接触发一个useReducer 来更新组件,上述代码片段中,我用 useState 代替 useReducer 了,如果相等,那么当前组件不需要更新,直接通知子代 Subscription ,检查子代 Subscription 是否更新,完成整个流程。

    四 实现异步

    基于 redux 异步的库有很多,最简单的 redux-thunk ,代码量少,只有几行,其中大量的逻辑需要开发者实现,还有比较复杂的 redux-saga ,基于 generator 实现,用起来稍微繁琐。
    对于完整的状态管理生态,大家可以尝试一下 dvajs ,它是基于 redux-saga 基础上,实现的异步的状态管理工具。dvajs 处理 reducers 也比较精妙,感兴趣的同学可以研究一下。

    五 总结

    通过本章节的学习,应该已经掌握一下内容:

  • 1 Redux 的基本概念和常用 API 。

  • 2 react-redux 基本用法,以及两种常用场景的实践 demo 。
  • 3 react-redux 原理实现。

下一节将学习 React 状态管理的另外一种方式 Mobx 。