写在前面
使用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,其中Provider和connect我们比较常见。当引入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。