React需要数据状态管理,因为组件化开发,不可避免的会遇到跨层级的数据通信,用一个统一的数据状态管理代替层层传入的props的方式更加利于维护和优雅。笔者认为,React不一定非得使用Redux,但凡实现了数据状态管理并且提供了足够的开放能力比如钩子、观察者、中间件等都可以与React结合,单纯来说,只实现了数据管理+观察者的工具便足以与React握手合作。至于为什么使用Redux,可能是因为他使用的广吧。本文将分析两者之间如何协作,以及解析社区已经封装好了的库react-redux。如果对redux感兴趣的话,可以翻阅上一篇文章从0到1重新撸一个redux。还是老样子,本文先不谈react-redux源码,先从应用场景出发

先把组件跟redux连接起来

熟悉react的开发者都知道,react主张单向数据流,通过改变state可以重新触发render更新视图,对于组件化开发模式,通过state数据传入子组件props来传递数据,通过父组件封装的setState改变state更新视图。所以说,核心都在于state以及改变state的方法。于是乎我们得出连接的方式:

  • 把store的数据跟state连接
  • 监听store更新来改变state
    来看看怎么实现吧:
  1. // redux module
  2. import {createStore} from 'redux'
  3. const initState = {
  4. text: 'hello world'
  5. }
  6. function reducer(state = initState, action) {
  7. switch(action.type) {
  8. case 'reverse_text':
  9. state.text = state.text.reverse();
  10. return state;
  11. default:
  12. return state;
  13. }
  14. }
  15. export const store = createStore(reducer, initState)
  1. import React from 'react'
  2. import { store } from './redux';
  3. export default class Header extends React.Component {
  4. constructor(props) {
  5. super(props);
  6. this.state = {
  7. text: store.getState().text
  8. }
  9. }
  10. componentDidMount() {
  11. store.subscribe(() => {
  12. console.log(store.getState())
  13. this.setState({
  14. text: store.getState().text
  15. })
  16. })
  17. }
  18. render() {
  19. return (
  20. <div className="hello-header">
  21. {this.state.text}
  22. </div>
  23. );
  24. }
  25. }

原理很简单,初始化数据从store读,调用subscribe监听store变化重新setState,让我们来看看在其他的组件里怎么更新Header组件吧

  1. import React from 'react'
  2. import { store } from './redux';
  3. export default class Bottom extends React.Component {
  4. constructor(props) {
  5. super(props);
  6. }
  7. componentDidMount() {
  8. console.log(store)
  9. }
  10. render() {
  11. return (
  12. <div className="hello-bottom">
  13. <button onClick={() => {
  14. store.dispatch({
  15. type: 'reverse_text'
  16. })
  17. }}>改变redux试试看</button>
  18. </div>
  19. );
  20. }
  21. }
import React from 'react'
import './App.css';
import Header from './header';
import Bottom from './bottom';

export default class App extends React.Component {
  constructor(props) {
    super(props);
  }

  render() { 
    return ( 
      <div className="hello-app">
        <Header />
        <Bottom />
      </div>
     );
  }
}

看吧,如此一来同级组件的数据通信问题美妙解决,但是还不够适用。因为上面的例子很短小,如果组件层级过多过深,层层传递props的做法简直难受。也需要把store一个个丢进去

更合理的连接方式

上面的设计固然可用,但是我们试想,一个大型的复杂项目,组件少说几十多辄上千,需要统一管理的数据肯定是非常多的,对于需要使用store数据的组件更是多如牛毛,不可能对这些组件每个都进行读store和注册监听吧。于是乎我们需要设计一种更加合理,无侵入式的连接方法,基于这个场景触发我们大胆提出以下功能。

  • 最顶层的组件统一挂载store,让后续所有层级的子组件都可以访问整个store
  • 对需要连接store的组件提供一个更友好的api,而非侵入式的写入constructor和生命周期

统一挂载store

看似非常简单,我们写出了以下代码:

import React from 'react'
import './App.css';
import Header from './header';
import Bottom from './bottom';
import { store } from './redux';

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
        store: store.getState()
    }
  }

  render() { 
    const {store} = this.state;
    return ( 
      <div className="hello-app">
        <Header store={store} />
        <Bottom store={store} />
      </div>
     );
  }
}

这勉强能行,但是如果Header&Bottom本身也是高阶组件,那么无法避免需要将store从最顶层不断通过props去传输,十分尴尬。这时候我们掀开react-redux的神秘面纱,抱歉久等了~

react-redux提供一个叫做Provider的组件,使用方法也十分简单

import React from 'react'
import './App.css';
import Header from './header';
import Bottom from './bottom';
import { store } from './redux';
import { Provider } from 'react-redux';

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
        store: store.getState()
    }
  }

  render() { 
    const {store} = this.state;
    return ( 
      <Provider store={store}>
        <Header />
        <Bottom />
      </Provider>
     );
  }
}

如此这般便能完美解决层层传递store的困境,究竟做了什么,翻开源码吧!

import React, { Component } from 'react'
import { ReactReduxContext } from './Context'
import Subscription from '../utils/Subscription'

const ReactReduxContext = React.createContext(null)

class Provider extends Component {
  constructor(props) {
    super(props)

    const { store } = props

    this.notifySubscribers = this.notifySubscribers.bind(this)
    const subscription = new Subscription(store)
    subscription.onStateChange = this.notifySubscribers

    this.state = {
      store,
      subscription
    }

    this.previousState = store.getState()
  }

  componentDidMount() {
    this._isMounted = true

    this.state.subscription.trySubscribe()

    if (this.previousState !== this.props.store.getState()) {
      this.state.subscription.notifyNestedSubs()
    }
  }

  componentWillUnmount() {
    if (this.unsubscribe) this.unsubscribe()

    this.state.subscription.tryUnsubscribe()

    this._isMounted = false
  }

  componentDidUpdate(prevProps) {
    if (this.props.store !== prevProps.store) {
      this.state.subscription.tryUnsubscribe()
      const subscription = new Subscription(this.props.store)
      subscription.onStateChange = this.notifySubscribers
      this.setState({ store: this.props.store, subscription })
    }
  }

  notifySubscribers() {
    this.state.subscription.notifyNestedSubs()
  }

  render() {
    const Context = this.props.context || ReactReduxContext

    return (
      <Context.Provider value={this.state}>
        {this.props.children}
      </Context.Provider>
    )
  }
}

export default Provider

最新的react-redux已经是更新到Hook版本,为了方便大家理解,用的是老版本7.0,别看代码多,关键点有两个

  • 从props接收store,并且设置为自身的state
  • React.createContext

createContext作用如下,也可以直接查看官方文档React.createContext
context.png

好了开始无侵入的让需要的组件进行连接 - connect

先看看怎么使用吧,再从使用的场景出去反推实现

import React from 'react'
import { store } from './redux';
import { connect } from 'react-redux'

class Header extends React.Component {
    constructor(props) {
        super(props);
    }

    render() {
        return (
            <div className="hello-header">
                {this.props.text}
                <button onClick={this.props.reverse}>reverse</button>
            </div>
        );
    }
}

const mapStateToProps = (state) => {
  return {
    text: state.text
  }
}
const mapDispatchToProps = (
  dispatch,
  ownProps
) => {
  return {
    reverse: () => {
      dispatch({
        type: 'reverse_text',
      });
    }
  };
}

const HeaderWrapper = connect(
  mapStateToProps,
  mapDispatchToProps
)(Header)

export default  HeaderWrapper

通过connect api 可以在mapStateToProps定义我们组件需要从store订阅的数据,通过mapDispatchToProps定义我们组件需要存在哪些可以更新store数据的方法,相比之前的做法简直完美,我们只需要专注组件本身的业务,十分松散不需耦合。
表面上看起来越是优雅大气,相对的,其背后必然隐藏着设计和实现越是复杂和深邃。connect就是如此,个人认为connect是react-redux最复杂和最难理解的部分。但是我们可以依样画葫芦实现最简单的版本。我们根据上面的代码可以合理推测出以下2点功能。

  • connect根据传入的mapStateToProps,mapDispatchToProps,Header返回一个高阶组件,并且mapStateToProps,mapDispatchToProps配置的属性和方法进行注入
  • 返回的高阶组件可以跟store相互通信。

可以如下简单实现

function connectHoc (mapStateToProps = () => ({}), mapDispatchToProps = () => ({})) {
  return function wrapWithConnect (wrappedComponent) {
    function connectFunction (props) {
      const [_, setState] = useState(0)
      const store = useContext(defaultContext)

      // 这里是为了每次store更新就更新state来刷新组件
      // 相当于就是非常粗暴的订阅store
      useEffect(() => {
        return store.subscribe(update)
      }, [])

      function update () {
        setState(times => ++times)
      }

      const stateProps = mapStateToProps(store.getState())

      const dispatchProps = mapDispatchToProps(store.dispatch)

      const allProps = {
          ...props,
          ...stateProps,
          ...dispatchProps
      }

      return <wrappedComponent {...allProps} />

    }
    // 为了阻止父组件render带来的不必要更新
    // 可以把他理解成更高级的React.PureComponent 
    return React.memo(ConnectFunction)
  }
}

以上代码能实现我们推测出的两个功能,但是非常粗暴,被我们CONNECT的组件,只要store更新了全都会被更新,而不是store更新了组件mapStateToProps订阅的数据才更新,我们需要connect能记住我们需要的数据,并且在store更新了这些数据的时候才进行组件更新。来看看怎么操作

因为要对比前后数据更新,先写一些判断数据的方法

// 判断基础类型强相等 排除掉0 和 1的布尔值自动转换带来的弊端
function is(x, y) {
  if (x === y) {
    return x !== 0 || y !== 0 || 1 / x === 1 / y
  } else {
    return x !== x && y !== y
  }
}

// 判断引用类型强相等
export function shallowEqual(objA, objB) {
  if (is(objA, objB)) return true

  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false
  }

  const keysA = Object.keys(objA)
  const keysB = Object.keys(objB)

  if (keysA.length !== keysB.length) return false

  // 遍历key判断是否相等 不相等返回false
  for (let i = 0; i < keysA.length; i++) {
    if (
      !Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
      !is(objA[keysA[i]], objB[keysA[i]])
    ) {
      return false 
    }
  }

  return true
}

// 单纯的强相等
export function strictEqual (a, b) {
  return a === b
}
import { shallowEqual, strictEqual } from './equals'

// 合并属性
function mergeProps (stateProps, dispatchProps, ownProps) {
  return { ...ownProps, ...stateProps, ...dispatchProps }
}

// 通过是否返回新的mergedProps来判断是否需要被更新
function mergedPropsFactory() {
  let hasOnceRun = false // 闭包保存是否执行过

  // 以下的属性都是记忆起来 方便通过前后对比
  let stateProps = null
  let dispatchProps = null
  let ownProps = null
  let mergedProps = null

  return (newStateProps, newDispatchProps, newOwnProps) => {

    // 第一次直接返回新的props 让他去渲染吧
    if (!hasOnceRun) {
      stateProps = newStateProps
      dispatchProps = newDispatchProps
      ownProps = newOwnProps
      mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
      hasOnceRun = true
      return mergedProps
    }

    // 前后对比判断是否需要被渲染
    if (shallowEqual(stateProps, newStateProps) && shallowEqual(ownProps, newOwnProps)) {
      stateProps = newStateProps
      dispatchProps = newDispatchProps
      ownProps = newOwnProps
    } else {
      stateProps = newStateProps
      dispatchProps = newDispatchProps
      ownProps = newOwnProps
      mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
    }

    return mergedProps
  }
}

最终的Connect

import React from 'react';
import mergedPropsFactory from './mergeProps.js';
import { useState, useRef, useMemo, useContext, useEffect } from 'react'
import {Context} from './provider';

export default function connectHoc(mapStateToProps = () => ({}), mapDispatchToProps = () => ({})) {
    return function WrapWithConnect(WrappedComponent) {
        function ConnectFunction(props) {
            const [_, setState] = useState(0)
            const store = useContext(Context) // 从provider消费获得store

            useEffect(() => {
                return store.subscribe(update) // 订阅store
            }, [])

            function update() {

                // 通过判断前后props是否保持相同引用来判断更新
                if (cacheAllProps.current === mergeProps(mapStateToProps(store.getState()), cacheDispatchProps.current, cacheOwnProps.current)) return
                setState(times => ++times)
            }

            // 用Hooks memo 当且仅当props发生改变时使用一次判断前后属性的方法
            const mergeProps = useMemo(() => (mergedPropsFactory()), [])
            const stateProps = mapStateToProps(store.getState())
            const dispatchProps = mapDispatchToProps(store.dispatch)
            const allProps = mergeProps(stateProps, dispatchProps, props)

            // 保持各种属性的记忆
            const cacheAllProps = useRef(null)
            const cacheOwnProps = useRef(null)
            const cacheStatePros = useRef(null)
            const cacheDispatchProps = useRef(null)

            useEffect(() => {
                // 勾子里更新属性记忆
                cacheAllProps.current = allProps
                cacheStatePros.current = stateProps
                cacheDispatchProps.current = dispatchProps
                cacheOwnProps.current = props
            }, [allProps])

            return <WrappedComponent {...allProps} />

        }
        // 为了阻止父组件render带来的不必要更新
        return React.memo(ConnectFunction)
    }
}

来看看效果吧:
最开始的效果
f1.png

点击header组件试试
f2.png

点击bottom组件试试
f3.png

可以知道 触发header组件改变相应的store props但是Bottom组件不会重新更新,反之亦然,互不影响。

放到最后再来谈谈容器组件和展示组件

为什么放到最后因为笔者认为这不是一个什么很硬核的内容,反而要多记一两个概念没要必要。前面说了,react的开发工作流核心都在于state以及改变state的方法,react-redux只是根据这个方法结合redux应用而生的罢了。用connect去连接组件,让他负责监听,监听完了把数据通过props传入简单的组件去展示。如此而已,并无奥妙。

总结。

前面学习了redux,再到结合redux和react的协作场景出发,大致把react-redux的工作流走了一遍,当然,react-redux不会这么简短,里面的设计还是非常复杂的,但是都逃不脱以上的步骤,官方react-redux使用了subscription去订阅布,判断和记忆前后Props的方式也不一样。最后分享一点感慨,不管是redux还是react-redux,都是从1 2处很小的使用场景和功能出发,但是为了能优雅的普适于诸多场景,其背后却充满设计。这点值得深思,这就是代码设计能用和很好用的差别吧。

如果本文有些许帮助 别忘了点赞哦,本文所有代码都在github可见mini-react-redux