react-redux 深度解析 (一)

814 阅读7分钟

写在前面

使用react也已经有差不多半年的时间了,在我们的项目中数据基本都是使用redux进行管理的。之前也将redux的源码看了一边,对于redux有了一个认识。但是redux毕竟可以和任何框架搭配,它只不过是一个数据管理。真正将它和react结合在一起的是react-redux,所以今天来分析一下react-redux源码。让我们开始吧!(配合redux食用更佳)

0、入口文件

整体学习一个源码前,我有一个习惯,就是先看它的入口文件,看看它对外输出了多少方法,然后通过实际的应用一步一步的分析。

import Provider from './components/Provider'
import connectAdvanced from './components/connectAdvanced'
import { ReactReduxContext } from './components/Context'
import connect from './connect/connect'

export { Provider, connectAdvanced, ReactReduxContext, connect }

这个是react-redux的入口文件,可以看到,它为我们提供了4个API,分别是Provider, connectAdvanced, ReactReduxContext, connect,其中Providerconnect我们比较常见。当引入redux的时候,一般的都会写以下这个样子。

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'

let store = createStore(todoApp)

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

这里我们使用到了第一个react-redux提供给我们的组件Provider,这个组件的作用是将store中的数据可以跨层级的进行共享,其实就是react的Context的作用,具体的请移步Context,这里就不详细介绍了。既然Provider是我们第一个接触到的API,那么我们就从它开始着手分析。

1、Provider.js

文件路径:src/component/Provider.js

我们先大致浏览以下Provider这个接口提供了什么

class Provider extends Component {
  constructor(props) {
   ···
  }

  componentDidMount() {
   ···
  }

  componentWillUnmount() {
   ···
  }

  componentDidUpdate(prevProps) {
   ···
  }

  subscribe() {
    ···
  }

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

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

这里我们先忽略函数具体实现的是什么功能,很明显的看到,这个就是一个十分平常的react组件,它包含了三个常见的组件周期钩子,和一个subscribe方法,以及render渲染。我们可以先来看看render函数,可以看到react-redux所提供的Provider是封装了Context的,这里它提供了两种Context的获取方式,一个是如果自己创建了Context,那我们可以通过以下方式获得

<Provider context = {mycontext}>
···
</Provider>

否则react-redux将会从ReactReduxContext中来获取,一起来看看ReactReduxContext是什么,在头部有这么一段代码,告知我们它是写在了Context.js中 (文件路径:src/component/Context.js)

import { ReactReduxContext } from './Context'

下面来看看这个Context.js都做了什么

import React from 'react'

export const ReactReduxContext = React.createContext(null)

export default ReactReduxContext

非常简单,就是创建了一个Context,这个和官网的例子一模一样,不多赘述。 回到Provider.js中,现在对于render已经有了一个认识,它就是将我们的App包裹在Context中,保证了我们不用一级一级通过props传递store。现在就是这个value了。 按照组件生命周期顺序我们逐个来分析。首先是constructor

···
constructor(props) {
    super(props)

    const { store } = props

    this.state = {
      storeState: store.getState(),
      store
    }
  }
···

在外部,传入的store = {store},在这里通过解构的方式取出store,同时设置state状态,它由两个参数,一个是

storeState:用来获取当前redux的状态

store:指向createStore之后return的对象。

在这里提一下,redux可以看作是一个状态机,它其实是通过闭包的方式保存了state的状态。

constructor执行之后,就该走到componentDidMount中了,先来看看这个函数,具体的作用我写在了注释中。

componentDidMount() {
    this._isMounted = true /*用来判断是否组件还在挂载中,之后的subscribe会用到*/
    this.subscribe() /* 通知组件进行更新 */
  }

在这里我们就接触到了subscribe这个方法了,那就来看看它的实现。

subscribe() {
    const { store } = this.props
    
    /* 传入一个callback在redux dispatch之后会通知观察者更新 */
    this.unsubscribe = store.subscribe(() => {
      const newStoreState = store.getState()
      
    /* 如果此时Provider组件已经解除挂载了,就return防止之后的setState报错 */
      if (!this._isMounted) {
        return
      }

    /* 如果上一次的State和本次获取的State一致,则不触发setState,防止组件渲染 */
      this.setState(providerState => {
        // If the value is the same, skip the unnecessary state update.
        if (providerState.storeState === newStoreState) {
          return null
        }

        return { storeState: newStoreState }
      })
    })

    /* 这里作者做了一层保险,如果我们在组件外部dispatch了,而此时组件正好在render时,就是触发上面函数的过程中,则获取最新的再次setState*/
    // Actions might have been dispatched between render and mount - handle those
    const postMountStoreState = store.getState()
    if (postMountStoreState !== this.state.storeState) {
      this.setState({ storeState: postMountStoreState })
    }
  }

这里大家可能会有一个疑问,最后为什么还要再次比较一下呢,其实,在社区有遇到这样一个情况,假设我们组件此时还未走到Mount的时候,在外部dispatch了一个action,而此时如果没有这个判断的话,实际的storeState已经更改了,但是subscribe只是将callback推入了观察者队列中,此时就会出现实际的State和Provider不同步的情况。

然后我们来看一下componentDidUpdate

componentDidUpdate(prevProps) {
    /*如果外部的store改变的话,先解绑再重新绑定 */
    if (this.props.store !== prevProps.store) {
      if (this.unsubscribe) this.unsubscribe()

      this.subscribe()
    }
  }
  

这里很简单,就是当我们

<Provider store={store}>
    <App />
  </Provider>

发生改变时重新进行新的store注册观察者,因为一般来说react全局只有一个store对象,因为state是通过闭包管理的,多个的话,就会出现数据混乱的情况。

最后componentWillUnmount

  componentWillUnmount() {
  /*当从组件树上解除挂载时,解绑store,同时设置isMounted,保证不再setState */
    if (this.unsubscribe) this.unsubscribe()

    this._isMounted = false
  }

以上就是Provider组件做的事情了。那么我们在react中的第一步就算是结束了。

2、connect.js

文件路径:src/connect/connect.js

当我们在各个组件中需要去和redux交互的话,我们会这么去使用

const mapStateToProps = state => {
  return {
    todos: getVisibleTodos(state.todos, state.visibilityFilter)
  }
}

const mapDispatchToProps = dispatch => {
  return {
    onTodoClick: id => {
      dispatch(toggleTodo(id))
    }
  }
}

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

这是一个常规用法,我们就从这里开始入手,首先看到这里的核心就是connect这个API,顾名思义,它就是连接redux的关键。同理我们先来大致看看它的组成。

export function createConnect({
  connectHOC = connectAdvanced, /*核心:用来包装我们的组件*/
  mapStateToPropsFactories = defaultMapStateToPropsFactories, /*MapStateToProps的工厂函数,提供三种转换规则*/
  mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,  /*mapDispatchToProps的工厂函数,提供三种转换规则*/
  mergePropsFactories = defaultMergePropsFactories,
  selectorFactory = defaultSelectorFactory
} = {}) {
  return function connect(
    mapStateToProps,
    mapDispatchToProps,
    mergeProps,
    {
      pure = true,
      areStatesEqual = strictEqual,
      areOwnPropsEqual = shallowEqual,
      areStatePropsEqual = shallowEqual,
      areMergedPropsEqual = shallowEqual,
      ...extraOptions
    } = {}
  ) {
    ···
    return connectHOC(selectorFactory, {
      ···
    })
  }
}

export default createConnect()

我们这里将函数简化了,我们先不关心它的实现,先来看看它的整体骨架。简单来说,createConnect闭包保存了react-redux内置的方法,返回了connect供外部使用。这些方法的作用我已经写在上面的注释中了。这里顺带讲解一下defaultMapStateToPropsFactories和defaultMapDispatchToPropsFactories的作用。它是将我们的mapDispatchToProps和mapStateToProps进行一层处理。

首先是defaultMapDispatchToPropsFactories,看过redux源码的都知道有一个文件叫做bindActionCreators,它是将我们的function包装成dispatch执行函数。 那在这里我们可以看到它的身影了 (文件路径src/connect/mapDispatchToProps.js) 具体的转换规则大家可以在这个文件中去看,它分为function和object来进行不同的封装。所以以下的mapDispatchToProps还有另一种写法


const toggleTodo = id => {
  return {
    type: 'TOGGLE_TODO',
    id
  }
}
-------------------------------------
const mapDispatchToProps = dispatch => {
  return {
    onTodoClick: id => {
      dispatch(toggleTodo(id))
    }
  }
}

等同于

const mapDispatchToProps = {
     onTodoClick: id => {
      toggleTodo(id)
    }
}
}

下面这样的情况实际上是通过redux进行了转换,大家可以参考redux的bindActionCreators源码。 再次回到connect中,我们来详细看看defaultMapDispatchToPropsFactories是如何对mapDispatchToProps进行处理的。

看下面这段createconnect中的代码

 const initMapDispatchToProps = match(
      mapDispatchToProps,
      mapDispatchToPropsFactories,
      'mapDispatchToProps'
    )

通过match对我们传入的mapDispatchToProps进行处理。

function match(arg, factories, name) {
  for (let i = factories.length - 1; i >= 0; i--) {
    const result = factories[i](arg)
    if (result) return result
  }

  return (dispatch, options) => {
    throw new Error(
      `Invalid value of type ${typeof arg} for ${name} argument when connecting component ${
        options.wrappedComponentName
      }.`
    )
  }
}

上面是match的实现。主要是将我们传入的mapDispatchToProps和defaultMapDispatchToPropsFactories提供的方法进行一个配对,factories就是defaultMapDispatchToPropsFactories中提供出来的方法集合。来一起看看具体的实现吧(文件路径:src/connect/mapDispatchToProps.js)

// 如果传入的mapDispatchToProps是一个函数就走这个函数
// wrapMapToPropsFunc的实现是通过proxy代理实现的
export function whenMapDispatchToPropsIsFunction(mapDispatchToProps) {
  return typeof mapDispatchToProps === 'function'
    ? wrapMapToPropsFunc(mapDispatchToProps, 'mapDispatchToProps')
    : undefined
}

//如果未传入mapDispatchToProps就走这个函数
export function whenMapDispatchToPropsIsMissing(mapDispatchToProps) {
  return !mapDispatchToProps
    ? wrapMapToPropsConstant(dispatch => ({ dispatch }))
    : undefined
}

// 如果传入的mapDispatchToProps是一个对象类型就走这个函数
export function whenMapDispatchToPropsIsObject(mapDispatchToProps) {
  return mapDispatchToProps && typeof mapDispatchToProps === 'object'
    ? wrapMapToPropsConstant(dispatch =>
        bindActionCreators(mapDispatchToProps, dispatch)
      )
    : undefined
}
/* 此处传出的是对外提供的三种match方法 */
export default [
  whenMapDispatchToPropsIsFunction,
  whenMapDispatchToPropsIsMissing,
  whenMapDispatchToPropsIsObject
]

这其中可以看到,bindActionCreators是redux中提供的方法,之前我们提到过,此处就不再赘述,我们可以写成对象类型的原因就是因为此处做了处理,调用了redux中的方法。这其中有一个关键的函数就是wrapMapToPropsConstant,来看看它的实现

export function wrapMapToPropsConstant(getConstant) {
  return function initConstantSelector(dispatch, options) {
    const constant = getConstant(dispatch, options)
    
    function constantSelector() {
      return constant
    }
    constantSelector.dependsOnOwnProps = false
    return constantSelector
  }
}

可以看到其实wrapMapToPropsConstant返回的是一个initConstantSelector函数,闭包保存了我们传入的函数。

回到connect.js中,以下match函数最后执行之后

为了方便分析,假设我们传入的mapDispatchToProps = undefined;
const initMapDispatchToProps = match(
      mapDispatchToProps,
      mapDispatchToPropsFactories,
      'mapDispatchToProps'
    )
    =========================>
    initMapDispatchToProps = function(dispatch, options) {
    getConstant = function(dispatch){
        return {dispatch}
    }
    const constant = getConstant(dispatch, options)
    function constantSelector() {
      return constant
    }
    constantSelector.dependsOnOwnProps = false
    return constantSelector
  }
    

我们回到最外层,再来看看我们的调用

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)
--------------------
目前我们分析了
connect(
  mapStateToProps,
  mapDispatchToProps
)
的具体流程,最终connect返回了一个connectHOC(),来看看connectHOC的实现

connectHOC(selectorFactory, {
      // used in error messages
      methodName: 'connect',

      // used to compute Connect's displayName from the wrapped component's displayName.
      getDisplayName: name => `Connect(${name})`,

      // if mapStateToProps is falsy, the Connect component doesn't subscribe to store state changes
      shouldHandleStateChanges: Boolean(mapStateToProps),

      // passed through to selectorFactory
      
      initMapStateToProps,
      initMapDispatchToProps,
      initMergeProps,
      pure,
      areStatesEqual,
      areOwnPropsEqual,
      areStatePropsEqual,
      areMergedPropsEqual,

      // any extra options args can override defaults of connect or connectAdvanced
      ...extraOptions
    })

以上selectorFactory是我们传入的react组件,同时我们将connect封装处理后的方法通过object集合的形式传入。最终底层执行connectHOC完成我们组件的注册redux。