共计 37909 个字符,预计需要花费 95 分钟才能阅读完成。
欢迎关注公众号:一口一个前端,不定期分享我所理解的前端知识
写在前面
我在读 React-Redux 源码的过程中,很自然的要去网上找一些参考文章,但发现这些文章基本都没有讲的很透彻,
很多时候就是平铺直叙把 API 挨个讲一下,而且只讲某一行代码是做什么的,却没有结合应用场景和用法解释清楚为什么这么做,加上源码本身又很抽象,
函数间的调用关系非常不好梳理清楚,最终结果就是越看越懵。我这次将尝试换一种解读方式,由最常见的用法入手,
结合用法,提出问题,带着问题看源码里是如何实现的,以此来和大家一起逐渐梳理清楚 React-Redux 的运行机制。
文章用了一周多的时间写完,粗看了一遍源码之后,又边看边写。源码不算少,我尽量把结构按照最容易理解的方式梳理,努力按照浅显的方式将原理讲出来,
但架不住代码结构的复杂,很多地方依然需要花时间思考,捋清函数之间的调用关系并结合用法才能明白。文章有点长,能看到最后的都是真爱~
水平有限,难免有地方解释的不到位或者有错误,也希望大家能帮忙指出来,不胜感激。
React-Redux 在项目中的应用
在这里,我就默认大家已经会使用 Redux 了,它为我们的应用提供一个全局的对象(store)来管理状态。
那么如何将 Redux 应用在 React 中呢?想一下,我们的最终目的是实现跨层级组件间通信与状态的统一管理。所以可以使用 Context 这个特性。
- 创建一个 Provider,将 store 传入 Provider,作为当前 context 的值,便于组件通过 context 获取 Redux store
- store 订阅一个组件更新的统一逻辑
- 组件需要更新数据时,需要调用 store.dispatch 派发 action,进而触发订阅的更新
- 组件获取数据时候,使用 store.getState()获取数据
而这些都需要自己手动去做,React-Redux 将上边的都封装起来了。让我们通过一段代码看一下 React-Redux 的用法:
首先是在 React 的最外层应用上,包裹 Provider,而 Provider 是 React-Redux 提供的组件,这里做的事情相当于上边的第一步
import React from 'react'
import {Provider} from 'react-redux'
import {createStore} from 'redux'
const reducer = (state, actions) => {...}
const store = createStore(reducer)
...
class RootApp extends React.Component {render() {
// 这里将 store 传入 Provider
return <Provider store={store}>
<App/>
</Provider>
}
}
第二步中的订阅,已经分别在 Provider 和 connect 中实现了
再看应用内的子组件。如果需要从 store 中拿数据或者更新 store 数据的话(相当于上边的第三步和第四步),
需要用 connect 将组件包裹起来:
import React from 'react'
import {connect} from '../../react-redux-src'
import {increaseAction, decreaseAction} from '../../actions/counter'
import {Button} from 'antd'
class Child extends React.Component {render() {const { increaseAction, decreaseAction, num} = this.props
return <div>
{num}
<Button onClick={() => increaseAction()}> 增加 </Button>
<Button onClick={() => decreaseAction()}> 减少 </Button>
</div>
}
}
const mapStateToProps = (state, ownProps) => {const { counter} = state
return {num: counter.num}
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {increaseAction: () => dispatch({type: INCREASE}),
decreaseAction: () => dispatch({type: DECREASE})
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Child)
mapStateToProps 用于建立组件和 store 中存储的状态的映射关系,它是一个函数,第一个参数是 state,也就是 redux 中存储的顶层数据,第二个参数是组件自身的 props。返回一个对象,对象内的字段就是该组件需要从 store 中获取的值。
mapDispatchToProps 用于建立组件和 store.dispatch 的映射关系。它可以是一个对象,也可以是一个函数,
当它是一个函数的时候,第一个参数就是 dispatch,第二个参数是组件自身的 props。
mapDispatchToProps 的对象形式如下:
const mapDispatchToProps = {increaseAction() {
return dispatch => dispatch({type: INCREASE})
},
decreaseAction() {
return dispatch => dispatch({type: DECREASE})
}
}
当不传 mapStateToProps 的时候,当 store 变化的时候,不会引起组件 UI 的更新。
当不传 mapDispatchToProps 的时候,默认将 dispatch 注入到组件的 props 中。
以上,如果 mapStateToProps 或者 mapDispatchToProps 传了 ownProps,那么在组件自身的 props 变化的时候,这两个函数也都会被调用。
React-Redux 做了什么
我们先给出结论,说明 React-Redux 做了什么工作:
- 提供 Subscrption 类,实现订阅更新的逻辑
- 提供 Provider,将 store 传入 Provider,便于下层组件从 context 或者 props 中获取 store;并订阅 store 的变化,便于在 store 变化的时候更新 Provider 自身
- 提供 selector,负责将获取 store 中的 stat 和 dispacth 一些 action 的函数(或者直接就是 dispatch)或者组件自己的 props,并从中选择出组件需要的值,作为返回值
-
提供 connect 高阶组件,主要做了两件事:
- 执行 selector,获取到要注入到组件中的值,将它们注入到组件的 props
- 订阅 props 的变化,负责在 props 变化的时候更新组件
如何做的
有了上边的结论,但想必大家都比较好奇究竟是怎么实现的,上边的几项工作都是协同完成的,最终的表象体现为下面几个问题:
- Provider 是怎么把 store 放入 context 中的
- 如何将 store 中的 state 和 dispatch(或者调用 dispatch 的函数)注入组件的 props 中的
- 我们都知道在 Redux 中,可以通过 store.subscribe()订阅一个更新页面的函数,来实现 store 变化,更新 UI,而 React-Redux 是如何做到 store 变化,被 connect 的组件也会更新的
接下来,带着这些问题来一条一条地分析源码。
Provider 是怎么把 store 放入 context 中的
先从 Provider 组件入手,代码不多,直接上源码
class Provider extends Component {constructor(props) {super(props)
// 从 props 中取出 store
const {store} = props
this.notifySubscribers = this.notifySubscribers.bind(this)
// 声明一个 Subscription 实例。订阅,监听 state 变化来执行 listener,都由实例来实现。const subscription = new Subscription(store)
// 绑定监听,当 state 变化时,通知订阅者更新页面
subscription.onStateChange = this.notifySubscribers
// 将 store 和 subscription 放入 state 中,稍后 this.state 将会作为 context 的 value
this.state = {
store,
subscription
}
// 获取当前的 store 中的 state,作为上一次的 state,将会在组件挂载完毕后,// 与 store 新的 state 比较,不一致的话更新 Provider 组件
this.previousState = store.getState()}
componentDidMount() {
this._isMounted = true
// 在组件挂载完毕后,订阅更新。至于如何订阅的,在下边讲到 Subscription 类的时候会讲到,// 这里先理解为最开始的时候需要订阅更新函数,便于在状态变化的时候更新 Provider 组件
this.state.subscription.trySubscribe()
// 如果前后的 store 中的 state 有变化,那么就去更新 Provider 组件
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) {
// 在组件更新的时候,检查一下当前的 store 与之前的 store 是否一致,若不一致,说明应该根据新的数据做变化,// 那么依照原来的数据做出改变已经没有意义了,所以会先取消订阅,再重新声明 Subscription 实例,// 绑定监听,设置 state 为新的数据
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() {// notifyNestedSubs() 实际上会通知让 listener 去执行,作用也就是更新 UI
this.state.subscription.notifyNestedSubs()}
render() {
const Context = this.props.context || ReactReduxContext
// 将 this.state 作为 context 的 value 传递下去
return (<Context.Provider value={this.state}>
{this.props.children}
</Context.Provider>
)
}
}
所以结合代码看这个问题:Provider 是怎么把 store 放入 context 中的,很好理解。
Provider 最主要的功能是从 props 中获取我们传入的 store,并将 store 作为 context 的其中一个值,向下层组件下发。
但是,一旦 store 变化,Provider 要有所反应,以此保证将始终将最新的 store 放入 context 中。所以这里要用订阅来实现更新。自然引出 Subscription 类,通过该类的实例,将 onStateChange 监听到一个可更新 UI 的事件 this.notifySubscribers
上:
subscription.onStateChange = this.notifySubscribers
组件挂载完成后,去订阅更新,至于这里订阅的是什么,要看 Subscription 的实现。这里先给出结论:本质上订阅的是onStateChange
,实现订阅的函数是:Subscription 类之内的trySubscribe
this.state.subscription.trySubscribe()
再接着,如果前后的 state 不一样,那么就去通知订阅者更新,onStateChange 就会执行,Provider 组件就会更新。走到更新完成(componentDidUpdate),
会去比较一下前后的 store 是否相同,如果不同,那么用新的 store 作为 context 的值,并且取消订阅,重新订阅一个新的 Subscription 实例。保证用的数据都是最新的。
所以说了这么多,其实这只是 Provider 组件的更新,而不是应用内部某个被 connect 的组件的更新机制。我猜想应该有一个原因是考虑到了 Provider 有可能被嵌套使用,所以会有这种在 Provider 更新之后取新数据并重新订阅的做法,这样才能保证每次传给子组件的 context 是最新的。
Subscription
我们已经发现了,Provider 组件是通过 Subscription 类中的方法来实现更新的,而过一会要讲到的 connect 高阶组件的更新,也是通过它来实现,可见 Subscription 是 React-Redux 实现订阅更新的核心机制。
import {getBatch} from './batch'
const CLEARED = null
const nullListeners = {notify() {}}
function createListenerCollection() {const batch = getBatch()
let current = []
let next = []
return {clear() {
// 清空 next 和 current
next = CLEARED
current = CLEARED
},
notify() {
// 将 next 赋值给 current,并同时赋值给 listeners,这里的 next、current、listeners 其实就是订阅的更新函数队列
const listeners = (current = next)
// 批量执行 listeners
batch(() => {for (let i = 0; i < listeners.length; i++) {
// 执行更新函数,这是触发 UI 更新的最根本的原理
listeners[i]()}
})
},
get() {return next},
subscribe(listener) {
let isSubscribed = true
// 将 current 复制一份,并赋值给 next,下边再向 next 中 push listener(更新页面的函数)if (next === current) next = current.slice()
next.push(listener)
return function unsubscribe() {if (!isSubscribed || current === CLEARED) return
isSubscribed = false
// 最终返回一个取消订阅的函数,用于在下一轮的时候清除没用的 listener
if (next === current) next = current.slice()
next.splice(next.indexOf(listener), 1)
}
}
}
}
export default class Subscription {constructor(store, parentSub) {
// 获取 store,要通过 store 来实现订阅
this.store = store
// 获取来自父级的 subscription 实例,主要是在 connect 的时候可能会用到
this.parentSub = parentSub
this.unsubscribe = null
this.listeners = nullListeners
this.handleChangeWrapper = this.handleChangeWrapper.bind(this)
}
addNestedSub(listener) {this.trySubscribe()
// 因为这里是被 parentSub 调用的,所以 listener 也会被订阅到 parentSub 上,也就是从 Provider 中获取的 subscription
return this.listeners.subscribe(listener)
}
notifyNestedSubs() {
// 通知 listeners 去执行
this.listeners.notify()}
handleChangeWrapper() {if (this.onStateChange) {
// onStateChange 会在外部的被实例化成 subcription 实例的时候,被赋值为不同的更新函数,被赋值的地方分别的 Provider 和 connect 中
// 由于刚刚被订阅的函数就是 handleChangeWrapper,而它也就相当于 listener。所以当状态变化的时候,listener 执行,onStateChange 会执行
this.onStateChange()}
}
isSubscribed() {return Boolean(this.unsubscribe)
}
trySubscribe() {if (!this.unsubscribe) {
// parentSub 实际上是 subcription 实例
// 这里判断的是 this.unsubscribe 被赋值后的值,本质上也就是判断 parentSub 有没有,顺便再赋值给 this.unsubscribe
// 如果 parentSub 没传,那么使用 store 订阅,否则,调用 parentSub.addNestedSub,使用 React-Redux 自己的订阅逻辑。具体会在代码下边的解释中说明
this.unsubscribe = this.parentSub
? this.parentSub.addNestedSub(this.handleChangeWrapper)
: this.store.subscribe(this.handleChangeWrapper)
// 创建 listener 集合
this.listeners = createListenerCollection()}
}
tryUnsubscribe() {
// 取消订阅
if (this.unsubscribe) {this.unsubscribe()
this.unsubscribe = null
this.listeners.clear()
this.listeners = nullListeners
}
}
}
Subscription 就是将页面的更新工作和状态的变化联系起来,具体就是 listener(触发页面更新的方法,在这里就是 handleChangeWrapper),通过 trySubscribe 方法,根据情况被分别订阅到 store 或者 Subscription 内部。放入到 listeners 数组,当 state 变化的时候,listeners 循环执行每一个监听器,触发页面更新。
说一下 trySubscribe 中根据不同情况判断直接使用 store 订阅,还是调用 addNestedSub 来实现内部订阅的原因。因为可能在一个应用中存在多个 store,这里的判断是为了让不同的 store 订阅自己的 listener,互不干扰。
如何向组件中注入 state 和 dispatch
将 store 从应用顶层注入后,该考虑如何向组件中注入 state 和 dispatch 了。
正常顺序肯定是先拿到 store,再以某种方式分别执行这两个函数,将 store 中的 state 和 dispatch,以及组件自身的 props 作为 mapStateToProps 和 mapDispatchToProps 的参数,传进去,我们就可以在这两个函数之内能拿到这些值。而它们的返回值,又会再注入到组件的 props 中。
说到这里,就要引出一个概念:selector。最终注入到组件的 props 是 selectorFactory 函数生成的 selector 的返回值,所以也就是说,mapStateToProps 和 mapDispatchToProps 本质上就是 selector。
生成的过程是在 connect 的核心函数 connectAdvanced 中,这个时候可以拿到当前 context 中的 store,进而用 store 传入 selectorFactory 生成 selector,其形式为
function selector(stateOrDispatch, ownProps) {
...
return props
}
通过形式可以看出:selector 就相当于 mapStateToProps 或者 mapDispatchToProps,selector 的返回值将作为 props 注入到组件中。
从 mapToProps 到 selector
标题的 mapToProps 泛指 mapStateToProps,mapDispatchToProps,mergeProps
结合日常的使用可知,我们的组件在被 connect 包裹之后才能拿到 state 和 dispatch,所以我们先带着上边的结论,单独梳理 selector 的机制,先看 connect 的源码:
export function createConnect({
connectHOC = connectAdvanced, // connectAdvanced 函数是 connect 的核心
mapStateToPropsFactories = defaultMapStateToPropsFactories,
mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
mergePropsFactories = defaultMergePropsFactories,
selectorFactory = defaultSelectorFactory
} = {}) {
return function connect(
mapStateToProps,
mapDispatchToProps,
mergeProps,
{...options} = {}) {
// 将我们传入的 mapStateToProps,mapDispatchToProps,mergeProps 都初始化一遍
const initMapStateToProps = match(mapStateToProps,
mapStateToPropsFactories,
'mapStateToProps')
const initMapDispatchToProps = match(mapDispatchToProps,
mapDispatchToPropsFactories,
'mapDispatchToProps')
const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')
// 返回 connectHOC 函数的调用,connectHOC 的内部是 connect 的核心
return connectHOC(selectorFactory, {
initMapStateToProps,
initMapDispatchToProps,
initMergeProps,
pure,
...
})
}
}
export default createConnect()
connect 实际上是 createConnect,createConnect 也只是返回了一个 connect 函数,而 connect 函数返回了 connectHOC 的调用(也就是 connectAdvanced 的调用),再继续,connectAdvanced 的调用最终会返回一个 wrapWithConnect 高阶组件,这个函数的参数是我们传入的组件。所以才有了 connect 平常的用法:
connect(mapStateToProps, mapDispatchToProps)(Component)
大家应该注意到了 connect 函数内将 mapStateToProps,mapDispatchToProps,mergeProps 都初始化了一遍,为什么要去初始化而不直接使用呢?带着疑问,我们往下看。
初始化 selector 过程
先看代码,主要看 initMapStateToProps 和 initMapDispatchToProps,看一下这段代码是什么意思。
const initMapStateToProps = match(mapStateToProps,
mapStateToPropsFactories,
'mapStateToProps')
const initMapDispatchToProps = match(mapDispatchToProps,
mapDispatchToPropsFactories,
'mapDispatchToProps')
const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')
mapStateToPropsFactories 和 mapDispatchToPropsFactories 都是函数数组,其中的每个函数都会接收一个参数,为 mapStateToProps 或者 mapDispatchToProps。而 match 函数的作用就是循环函数数组,mapStateToProps 或者 mapDispatchToProps 作为每个函数的入参去执行,当此时的函数返回值不为假的时候,赋值给左侧。看一下 match 函数:
function match(arg, factories, name) {
// 循环执行 factories,这里的 factories 也就是 mapStateToProps 和 mapDisPatchToProps 两个文件中暴露出来的处理函数数组
for (let i = factories.length - 1; i >= 0; i--) {
// arg 也就是 mapStateToProps 或者 mapDispatchToProps
// 这里相当于将数组内的每个函数之星了一遍,并将我们的 mapToProps 函数作为参数传进去
const result = factories[i](arg)
if (result) return result
}
}
match 循环的是一个函数数组,下面我们看一下这两个数组,分别是 mapStateToPropsFactories 和 mapDispatchToPropsFactories:
(下边源码中的 whenMapStateToPropsIsFunction 函数会放到后边讲解)
-
mapStateToPropsFactories
-
import {wrapMapToPropsConstant, wrapMapToPropsFunc} from './wrapMapToProps' // 当 mapStateToProps 是函数的时候,调用 wrapMapToPropsFunc export function whenMapStateToPropsIsFunction(mapStateToProps) { return typeof mapStateToProps === 'function' ? wrapMapToPropsFunc(mapStateToProps, 'mapStateToProps') : undefined } // 当 mapStateToProps 没有传的时候,调用 wrapMapToPropsConstant export function whenMapStateToPropsIsMissing(mapStateToProps) {return !mapStateToProps ? wrapMapToPropsConstant(() => ({})) : undefined } export default [whenMapStateToPropsIsFunction, whenMapStateToPropsIsMissing]
实际上是让
whenMapStateToPropsIsFunction
和whenMapStateToPropsIsMissing
都去执行一次 mapStateToProps,然后根据传入的 mapStateToProps 的情况来选出有执行结果的函数赋值给 initMapStateToProps。单独看一下 whenMapStateToPropsIsMissing
export function wrapMapToPropsConstant(getConstant) {return function initConstantSelector(dispatch, options) {const constant = getConstant(dispatch, options) function constantSelector() {return constant} constantSelector.dependsOnOwnProps = false return constantSelector } }
wrapMapToPropsConstant 返回了一个函数,接收的参数是我们传入的 () => ({}),函数内部调用了入参函数并赋值给一个常量放入了 constantSelector 中,
该常量实际上就是我们不传 mapStateToProps 时候的生成的 selector,这个 selector 返回的是空对象,所以不会接受任何来自 store 中的 state。同时可以看到 constantSelector.dependsOnOwnProps = false,表示返回值与 connect 高阶组件接收到的 props 无关。
-
-
mapDispatchToPropsFactories
-
import {bindActionCreators} from '../../redux-src' import {wrapMapToPropsConstant, wrapMapToPropsFunc} from './wrapMapToProps' export function whenMapDispatchToPropsIsFunction(mapDispatchToProps) { return typeof mapDispatchToProps === 'function' ? wrapMapToPropsFunc(mapDispatchToProps, 'mapDispatchToProps') : undefined } // 当不传 mapDispatchToProps 时,默认向组件中注入 dispatch export function whenMapDispatchToPropsIsMissing(mapDispatchToProps) { return !mapDispatchToProps ? wrapMapToPropsConstant(dispatch => ({ dispatch})) : undefined } // 当传入的 mapDispatchToProps 是对象,利用 bindActionCreators 进行处理 详见 redux/bindActionCreators.js export function whenMapDispatchToPropsIsObject(mapDispatchToProps) { return mapDispatchToProps && typeof mapDispatchToProps === 'object' ? wrapMapToPropsConstant(dispatch => bindActionCreators(mapDispatchToProps, dispatch)) : undefined } export default [ whenMapDispatchToPropsIsFunction, whenMapDispatchToPropsIsMissing, whenMapDispatchToPropsIsObject ]
没有传递 mapDispatchToProps 的时候,会调用 whenMapDispatchToPropsIsMissing,这个时候,constantSelector 只会返回一个 dispatch,所以只能在组件中接收到 dispatch。
当传入的 mapDispatchToProps 是对象的时候,也是调用 wrapMapToPropsConstant,根据前边的了解,这里注入到组件中的属性是
bindActionCreators(mapDispatchToProps, dispatch) 的执行结果。
-
现在,让我们看一下 whenMapStateToPropsIsFunction 这个函数。它是在 mapDispatchToProps 与 mapStateToProps 都是函数的时候调用的,实现也比较复杂。这里只单用 mapStateToProps 来举例说明。
再提醒一下:下边的 mapToProps 指的是 mapDispatchToProps 或 mapStateToProps
// 根据 mapStateToProps 函数的参数个数,判断组件是否应该依赖于自己的 props
export function getDependsOnOwnProps(mapToProps) {
return mapToProps.dependsOnOwnProps !== null && mapToProps.dependsOnOwnProps !== undefined
? Boolean(mapToProps.dependsOnOwnProps)
: mapToProps.length !== 1
}
export function wrapMapToPropsFunc(mapToProps, methodName) {
// 最终 wrapMapToPropsFunc 返回的是一个 proxy 函数,返回的函数会在 selectorFactory 函数中
// 的 finalPropsSelectorFactory 内被调用并赋值给其他变量。// 而这个 proxy 函数会在 selectorFactory 中执行,生成最终的 selector
return function initProxySelector(dispatch, { displayName}) {const proxy = function mapToPropsProxy(stateOrDispatch, ownProps) {
// 根据组件是否依赖自身的 props 决定调用的时候传什么参数
return proxy.dependsOnOwnProps
? proxy.mapToProps(stateOrDispatch, ownProps)
: proxy.mapToProps(stateOrDispatch)
}
proxy.dependsOnOwnProps = true
proxy.mapToProps = function detectFactoryAndVerify(stateOrDispatch, ownProps) {
// 将 proxy.mapToProps 赋值为我们传入的 mapToProps
proxy.mapToProps = mapToProps
// 根据组件是否传入了组件本身从父组件接收的 props 来确定是否需要向组件中注入 ownProps,// 最终会用来实现组件自身的 props 变化,也会调用 mapToProps 的效果
proxy.dependsOnOwnProps = getDependsOnOwnProps(mapToProps)
// 再去执行 proxy,这时候 proxy.mapToProps 已经被赋值为我们传进来的 mapToProps 函数,// 所以 props 就会被赋值成传进来的 mapToProps 的返回值
let props = proxy(stateOrDispatch, ownProps)
if (typeof props === 'function') {
// 如果返回值是函数,那么再去执行这个函数,再将 store 中的 state 或 dispatch,以及 ownProps 再传进去
proxy.mapToProps = props
proxy.dependsOnOwnProps = getDependsOnOwnProps(props)
props = proxy(stateOrDispatch, ownProps)
}
if (process.env.NODE_ENV !== 'production')
verifyPlainObject(props, displayName, methodName)
return props
}
return proxy
}
}
wrapMapToPropsFunc 返回的实际上是 initProxySelector 函数,initProxySelector 的执行结果是一个代理 proxy,可理解为将传进来的数据(state 或 dispatch,ownProps)代理到我们传进来的 mapToProps 函数。proxy 的执行结果是 proxy.mapToProps,本质就是 selector。
页面初始化执行的时候,dependsOnOwnProps 为 true,所以执行 proxy.mapToProps(stateOrDispatch, ownProps),也就是 detectFactoryAndVerify。在后续的执行过程中,会先将 proxy 的 mapToProps 赋值为我们传入 connect 的 mapStateToProps 或者 mapDispatchToProps,然后在依照实际情况组件是否应该依赖自己的 props 赋值给 dependsOnOwnProps。(注意,这个变量会在 selectorFactory 函数中作为组件是否根据自己的 props 变化执行 mapToProps 函数的依据)。
总结一下,这个函数最本质上做的事情就是将我们传入 connect 的 mapToProps 函数挂到 proxy.mapToProps 上,同时再往 proxy 上挂载一个 dependsOnOwnProps 来方便区分组件是否依赖自己的 props。最后,proxy 又被作为 initProxySelector 的返回值,所以初始化过程被赋值的 initMapStateToProps、initMapDispatchToProps、initMergeProps 实际上是 initProxySelector 的函数引用,它们执行之后是 proxy,至于它们三个 proxy 是在哪执行来生成具体的 selector 的我们下边会讲到。
现在,回想一下我们的疑问,为什么要去初始化那三个 mapToProps 函数?目的很明显,就是准备出生成 selector 的函数,用来放到一个合适的时机来执行,同时决定 selector 要不要对 ownProps 的改变做反应。
创建 selector,向组件注入 props
准备好了生成 selector 的函数之后,就需要执行它,将它的返回值作为 props 注入到组件中了。先粗略的概括一下注入的过程:
- 取到 store 的 state 或 dispatch,以及 ownProps
- 执行 selector
- 将执行的返回值注入到组件
下面我们需要从最后一步的注入开始倒推,来看 selector 是怎么执行的。
注入的过程发生在 connect 的核心函数 connectAdvanced 之内,先忽略该函数内的其他过程,聚焦注入过程,简单看下源码
export default function connectAdvanced(
selectorFactory,
{getDisplayName = name => `ConnectAdvanced(${name})`,
methodName = 'connectAdvanced',
renderCountProp = undefined,
shouldHandleStateChanges = true,
storeKey = 'store',
withRef = false,
forwardRef = false,
context = ReactReduxContext,
...connectOptions
} = {}) {
const Context = context
return function wrapWithConnect(WrappedComponent) {
// ... 忽略了其他代码
// selectorFactoryOptions 是包含了我们初始化的 mapToProps 的一系列参数
const selectorFactoryOptions = {
...connectOptions,
getDisplayName,
methodName,
renderCountProp,
shouldHandleStateChanges,
storeKey,
displayName,
wrappedComponentName,
WrappedComponent
}
// pure 表示只有当 state 或者 ownProps 变动的时候,重新计算生成 selector。const {pure} = connectOptions
/* createChildSelector 的调用形式:createChildSelector(store)(state, ownProps),createChildSelector 返回了 selectorFactory 的调用,而 selectorFactory 实际上是其内部根据 options.pure 返回的
impureFinalPropsSelectorFactory 或者是 pureFinalPropsSelectorFactory 的调用,而这两个函数需要的参数是
mapStateToProps,
mapDispatchToProps,
mergeProps,
dispatch,
options
除了 dispatch,其余参数都可从 selectorFactoryOptions 中获得。调用的返回值,就是 selector。而 selector 需要的参数是
(state, ownprops)。所以得出结论,createChildSelector(store)就是 selector
*/
function createChildSelector(store) {
// 这里是 selectorFactory.js 中 finalPropsSelectorFactory 的调用(本质上也就是上面我们初始化的 mapToProps 的调用),传入 dispatch,和 options
return selectorFactory(store.dispatch, selectorFactoryOptions)
}
function ConnectFunction(props) {
const store = props.store || contextValue.store
// 仅当 store 变化的时候,创建 selector
// 调用 childPropsSelector => childPropsSelector(dispatch, options)
const childPropsSelector = useMemo(() => {
// 每当 store 变化的时候重新创建这个选择器
return createChildSelector(store)
}, [store])
// actualChildProps 就是最终要注入到组件中的 props,也就是 selector 的返回值。const actualChildProps = usePureOnlyMemo(() => {return childPropsSelector(store.getState(), wrapperProps)
}, [store, previousStateUpdateResult, wrapperProps])
const renderedWrappedComponent = useMemo(
// 这里是将 props 注入到组件的地方
() => <WrappedComponent {...actualChildProps} />,
[forwardedRef, WrappedComponent, actualChildProps]
)
}
// 最后 return 出去
return hoistStatics(Connect, WrappedComponent)
}
在注入过程中,有一个很重要的东西:selectorFactory
。这个函数就是生成 selector 的很重要的一环。它起到一个上传下达的作用,把接收到的 dispatch,以及那三个 mapToProps 函数,传入到 selectorFactory 内部的处理函数(pureFinalPropsSelectorFactory 或 impureFinalPropsSelectorFactory)中,selectorFactory 的执行结果是内部处理函数的调用。而内部处理函数的执行结果就是将那三种 selector(mapStateToProps,mapDispatchToProps,mergeProps)
执行后合并的结果。也就是最终要传给组件的 props
下面我们看一下 selectorFactory 的内部实现。为了清晰,只先一下内部的结构
// 直接将 mapStateToProps,mapDispatchToProps,ownProps 的执行结果合并作为返回值 return 出去
export function impureFinalPropsSelectorFactory(){}
export function pureFinalPropsSelectorFactory() {
// 整个过程首次初始化的时候调用
function handleFirstCall(firstState, firstOwnProps) {}
// 返回新的 props
function handleNewPropsAndNewState() {// 将 mapStateToProps,mapDispatchToProps,ownProps 的执行结果合并作为返回值 return 出去}
// 返回新的 props
function handleNewProps() {// 将 mapStateToProps,mapDispatchToProps,ownProps 的执行结果合并作为返回值 return 出去}
// 返回新的 props
function handleNewState() {// 将 mapStateToProps,mapDispatchToProps,ownProps 的执行结果合并作为返回值 return 出去}
// 后续的过程调用
function handleSubsequentCalls(nextState, nextOwnProps) {}
return function pureFinalPropsSelector(nextState, nextOwnProps) {
// 第一次渲染,调用 handleFirstCall,之后的 action 派发行为会触发 handleSubsequentCalls
return hasRunAtLeastOnce
? handleSubsequentCalls(nextState, nextOwnProps)
: handleFirstCall(nextState, nextOwnProps)
}
}
// finalPropsSelectorFactory 函数是在 connectAdvaced 函数内调用的 selectorFactory 函数
export default function finalPropsSelectorFactory(
dispatch,
{initMapStateToProps, initMapDispatchToProps, initMergeProps, ...options}
) {const mapStateToProps = initMapStateToProps(dispatch, options)
// 这里是 wrapMapToProps.js 中 wrapMapToPropsFunc 函数的柯里化调用,是改造
// 之后的 mapStateToProps, 在下边返回的函数内还会再调用一次
const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
const mergeProps = initMergeProps(dispatch, options)
// 根据是否传入 pure 属性,决定调用哪个生成 selector 的函数来计算传给组件的 props。并将匹配到的函数赋值给 selectorFactory
const selectorFactory = options.pure
? pureFinalPropsSelectorFactory // 当 props 或 state 变化的时候,才去重新计算 props
: impureFinalPropsSelectorFactory // 直接重新计算 props
// 返回 selectorFactory 的调用
return selectorFactory(
mapStateToProps,
mapDispatchToProps,
mergeProps,
dispatch,
options
)
}
可以看出来,selectorFactory 内部会决定在什么时候生成新的 props。下面来看一下完整的源码
export function impureFinalPropsSelectorFactory(
mapStateToProps,
mapDispatchToProps,
mergeProps,
dispatch
) {
// 如果调用这个函数,直接将三个 selector 的执行结果合并返回
return function impureFinalPropsSelector(state, ownProps) {
return mergeProps(mapStateToProps(state, ownProps),
mapDispatchToProps(dispatch, ownProps),
ownProps
)
}
}
export function pureFinalPropsSelectorFactory(
mapStateToProps,
mapDispatchToProps,
mergeProps,
dispatch,
{areStatesEqual, areOwnPropsEqual, areStatePropsEqual}
) {
// 使用闭包保存一个变量,标记是否是第一次执行
let hasRunAtLeastOnce = false
// 下边这些变量用于缓存计算结果
let state
let ownProps
let stateProps
let dispatchProps
let mergedProps
function handleFirstCall(firstState, firstOwnProps) {
state = firstState
ownProps = firstOwnProps
// 这里是 wrapMapToProps.js 中 wrapMapToPropsFunc 函数的柯里化调用的函数内部的 proxy 函数的调用。stateProps = mapStateToProps(state, ownProps)
/*
* 膝盖已烂,太绕了
* 回顾一下 proxy:
* const proxy = function mapToPropsProxy(stateOrDispatch, ownProps) {}
* return proxy
* */
dispatchProps = mapDispatchToProps(dispatch, ownProps)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
hasRunAtLeastOnce = true
// 返回计算后的 props
return mergedProps
}
function handleNewPropsAndNewState() {stateProps = mapStateToProps(state, ownProps)
// 由于这个函数的调用条件是 ownProps 和 state 都变化,所以有必要判断一下 dependsOnOwnProps
if (mapDispatchToProps.dependsOnOwnProps)
dispatchProps = mapDispatchToProps(dispatch, ownProps)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
return mergedProps
}
function handleNewProps() {
// 判断如果需要依赖组件自己的 props,重新计算 stateProps
if (mapStateToProps.dependsOnOwnProps) {stateProps = mapStateToProps(state, ownProps)
}
// 同上
if (mapDispatchToProps.dependsOnOwnProps)
dispatchProps = mapDispatchToProps(dispatch, ownProps)
// 将组件自己的 props,dispatchProps,stateProps 整合出来
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
return mergedProps
}
function handleNewState() {const nextStateProps = mapStateToProps(state, ownProps)
const statePropsChanged = !areStatePropsEqual(nextStateProps, stateProps)
stateProps = nextStateProps
// 由于 handleNewState 执行的大前提是 pure 为 true,所以有必要判断一下前后来自 store 的 state 是否变化
if (statePropsChanged)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
return mergedProps
}
function handleSubsequentCalls(nextState, nextOwnProps) {const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps)
const stateChanged = !areStatesEqual(nextState, state)
state = nextState
ownProps = nextOwnProps
// 依据不同的情况,调用不同的函数
if (propsChanged && stateChanged) return handleNewPropsAndNewState() // 当组件自己的 props 和注入的 store 中的某些 state 同时变化时,调用 handleNewPropsAndNewState()获取最新的 props
if (propsChanged) return handleNewProps() // 仅当组件自己的 props 变化时,调用 handleNewProps 来获取最新的 props,此时的 props 包括注入的 props,组件自身的 props,和 dpspatch 内的函数
if (stateChanged) return handleNewState() // 仅当注入的 store 中的某些 state 变化时,调用 handleNewState()获取最新的 props, 此时的 props 包括注入的 props,组件自身的 props,和 dpspatch 内的函数
// 如果都没变化,直接返回先前缓存的 mergedProps,并且在以上三个函数中,都分别用闭包机制对数据做了缓存
return mergedProps
}
return function pureFinalPropsSelector(nextState, nextOwnProps) {
// 第一次渲染,调用 handleFirstCall,之后的 action 派发行为会触发 handleSubsequentCalls
return hasRunAtLeastOnce
? handleSubsequentCalls(nextState, nextOwnProps)
: handleFirstCall(nextState, nextOwnProps)
}
}
export default function finalPropsSelectorFactory(
dispatch,
{initMapStateToProps, initMapDispatchToProps, initMergeProps, ...options}
) {const mapStateToProps = initMapStateToProps(dispatch, options) // 这里是 wrapMapToProps.js 中 wrapMapToPropsFunc 函数的柯里化调用,是改造
// 之后的 mapStateToProps, 在下边返回的函数内还会再调用一次
const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
const mergeProps = initMergeProps(dispatch, options)
// 验证 mapToProps 函数,有错误时给出提醒
if (process.env.NODE_ENV !== 'production') {
verifySubselectors(
mapStateToProps,
mapDispatchToProps,
mergeProps,
options.displayName
)
}
// 根据是否传入了 pure,决定计算新 props 的方式,默认为 true
const selectorFactory = options.pure
? pureFinalPropsSelectorFactory
: impureFinalPropsSelectorFactory
return selectorFactory(
mapStateToProps,
mapDispatchToProps,
mergeProps,
dispatch,
options
)
}
至此,我们搞明白了 mapToProps 函数是在什么时候执行的。再来回顾一下这部分的问题:如何向组件中注入 state 和 dispatch,让我们从头梳理一下:
传入 mapToProps
首先,在 connect 的时候传入了 mapStateToProps,mapDispatchToProps,mergeProps。再联想一下用法,这些函数内部可以接收到 state 或 dispatch,以及 ownProps,它们的返回值会传入组件的 props。
基于 mapToProps 生成 selector
需要根据 ownProps 决定是否要依据其变化重新计算这些函数的返回值,所以会以这些函数为基础,生成代理函数(proxy),代理函数的执行结果就是 selector,上边挂载了 dependsOnOwnProps 属性,所以在 selectorFactory 内真正执行的时候,才有何时才去重新计算的依据。
将 selector 的执行结果作为 props 传入组件
这一步在 connectAdvanced 函数内,创建一个调用 selectorFactory,将 store 以及初始化后的 mapToProps 函数和其他配置传进去。selectorFactory 内执行 mapToProps(也就是 selector),获取返回值,最后将这些值传入组件。
大功告成
React-Redux 的更新机制
React-Redux 的更新机制也是属于订阅发布的模式。而且与 Redux 类似,一旦状态发生变化,调用 listener 更新页面。让我们根据这个过程抓取关键点:
- 更新谁?
- 订阅的更新函数是什么?
- 如何判断状态变化?
不着急看代码,我觉得先用文字描述清楚这些关键问题,不再一头雾水地看代码更容易让大家理解。
更新谁?
回想一下平时使用 React-Redux 的时候,是不是只有被 connect 过并且传入了 mapStateToProps 的组件,会响应 store 的变化?
所以,被更新的是被 connect 过的组件,而 connect 返回的是 connectAdvanced,并且并且 connectAdvanced 会返回我们传入的组件,
所以本质上是 connectAdvanced 内部依据 store 的变化更新自身,进而达到更新真正组件的目的。
订阅的更新函数是什么?
这一点从 connectAdvanced 内部订阅的时候可以很直观地看出来:
subscription.onStateChange = checkForUpdates
subscription.trySubscribe()
订阅的函数是checkForUpdates
,重要的是这个 checkForUpdates 做了什么,能让组件更新。在 connectAdvanced 中使用 useReducer 内置了一个 reducer,这个函数做的事情就是在前置条件(状态变化)成立的时候,dispatch 一个 action,来触发更新。
如何判断状态变化?
这个问题很好理解,因为每次 redux 返回的都是一个新的 state。直接判断前后的 state 的引用是否相同,就可以了
connect 核心 –connectAdvanced
connectAdvanced 是一个比较重量级的高阶函数,上边大致说了更新机制,但很多具体做法都是在 connectAdvanced 中实现的。源码很长,逻辑有一些复杂,我写了详细的注释。看的过程需要思考函数之间的调用关系以及目的,每个变量的意义,带着上边的结论,相信不难看懂。
// 这是保留组件的静态方法的库
import hoistStatics from 'hoist-non-react-statics'
import React, {
useContext,
useMemo,
useEffect,
useLayoutEffect,
useRef,
useReducer
} from 'react'
import {isValidElementType, isContextConsumer} from 'react-is'
import Subscription from '../utils/Subscription'
import {ReactReduxContext} from './Context'
const EMPTY_ARRAY = []
const NO_SUBSCRIPTION_ARRAY = [null, null]
// 内置的 reducer
function storeStateUpdatesReducer(state, action) {const [, updateCount] = state
return [action.payload, updateCount + 1]
}
const initStateUpdates = () => [null, 0]
// React currently throws a warning when using useLayoutEffect on the server.
// To get around it, we can conditionally useEffect on the server (no-op) and
// useLayoutEffect in the browser. We need useLayoutEffect because we want
// `connect` to perform sync updates to a ref to save the latest props after
// a render is actually committed to the DOM.
// 自己对于以上英文注释的意译:// 当在服务端环境使用 useLayoutEffect 时候,react 会发出警告,为了解决此问题,需要在服务端使用 useEffect,浏览器端使用 useLayoutEffect。// useLayoutEffect 会在所有的 DOM 变更之后同步调用传入其中的回调(effect),// 所以在浏览器环境下需要使用它,因为 connect 将会在渲染被提交到 DOM 之后,再同步更新 ref 来保存最新的 props
// ReactHooks 文档对 useLayoutEffect 的说明:在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。// useEffect 的 effect 将在每轮渲染结束后执行,useLayoutEffect 的 effect 在 dom 变更之后,绘制之前执行。// 这里的 effect 做的是更新工作
// 在服务端渲染的时候页面已经出来了,有可能 js 还未加载完成。// 所以需要在 SSR 阶段使用 useEffect,保证在页面由 js 接管后,如果需要更新了,再去更新。// 而在浏览器环境则不存在这样的问题
// 根据是否存在 window 确定是服务端还是浏览器端
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect
export default function connectAdvanced(
selectorFactory,
// options object:
{
// 获取被 connect 包裹之后的组件名
getDisplayName = name => `ConnectAdvanced(${name})`,
// 为了报错信息的显示
methodName = 'connectAdvanced',
// 直接翻译的英文注释:如果被定义, 名为此值的属性将添加到传递给被包裹组件的 props 中。它的值将是组件被渲染的次数,这对于跟踪不必要的重新渲染非常有用。默认值: undefined
renderCountProp = undefined,
// connect 组件是否应响应 store 的变化
shouldHandleStateChanges = true,
// 使用了多个 store 的时候才需要用这个,目的是为了区分该获取哪个 store
storeKey = 'store',
// 如果为 true,则将一个引用存储到被包裹的组件实例中,// 并通过 getWrappedInstance()获取到。withRef = false,
// 用于将 ref 传递进来
forwardRef = false,
// 组件内部使用的 context,用户可自定义
context = ReactReduxContext,
// 其余的配置项,selectorFactory 应该会用到
...connectOptions
} = {}) {
// 省略了一些报错的逻辑
// 获取 context
const Context = context
return function wrapWithConnect(WrappedComponent) {
const wrappedComponentName =
WrappedComponent.displayName || WrappedComponent.name || 'Component'
const displayName = getDisplayName(wrappedComponentName)
// 定义 selectorFactoryOptions,为构造 selector 做准备
const selectorFactoryOptions = {
...connectOptions,
getDisplayName,
methodName,
renderCountProp,
shouldHandleStateChanges,
storeKey,
displayName,
wrappedComponentName,
WrappedComponent
}
const {pure} = connectOptions
/* 调用 createChildSelector => createChildSelector(store)(state, ownProps)
createChildSelector 返回了 selectorFactory 的带参调用,而 selectorFactory 实际上是其内部根据 options.pure 返回的
impureFinalPropsSelectorFactory 或者是 pureFinalPropsSelectorFactory 的调用,而这两个函数需要的参数是(state, ownProps)
*/
function createChildSelector(store) {
// 这里是 selectorFactory.js 中 finalPropsSelectorFactory 的调用,传入 dispatch,和 options
return selectorFactory(store.dispatch, selectorFactoryOptions)
}
// 根据是否是 pure 模式来决定是否需要对更新的方式做优化,pure 在这里的意义类似于 React 的 PureComponent
const usePureOnlyMemo = pure ? useMemo : callback => callback()
function ConnectFunction(props) {
// props 变化,获取最新的 context,forwardedRef 以及组件其他 props
const [propsContext, forwardedRef, wrapperProps] = useMemo(() => {const { context, forwardedRef, ...wrapperProps} = props
return [context, forwardedRef, wrapperProps]
}, [props])
// propsContext 或 Context 发生变化,决定使用哪个 context,如果 propsContext 存在则优先使用
const ContextToUse = useMemo(() => {
// 用户可能会用自定义的 context 来代替 ReactReduxContext,缓存住我们应该用哪个 context 实例
// Users may optionally pass in a custom context instance to use instead of our ReactReduxContext.
// Memoize the check that determines which context instance we should use.
return propsContext &&
propsContext.Consumer &&
isContextConsumer(<propsContext.Consumer />)
? propsContext
: Context
}, [propsContext, Context])
// 通过上层组件获取上下文中的 store
// 当上层组件最近的 context 变化的时候,返回该 context 的当前值,也就是 store
const contextValue = useContext(ContextToUse)
// store 必须存在于 prop 或者 context 中
// 判断 store 是否是来自 props 中的 store
const didStoreComeFromProps = Boolean(props.store)
// 判断 store 是否是来自 context 中的 store
const didStoreComeFromContext =
Boolean(contextValue) && Boolean(contextValue.store)
// 从 context 中取出 store,准备被 selector 处理之后注入到组件。优先使用 props 中的 store
const store = props.store || contextValue.store
// 仅当 store 变化的时候,创建 selector
// childPropsSelector 调用方式:childPropsSelector(dispatch, options)
const childPropsSelector = useMemo(() => {
// selector 的创建需要依赖于传入 store
// 每当 store 变化的时候重新创建这个 selector
return createChildSelector(store)
}, [store])
const [subscription, notifyNestedSubs] = useMemo(() => {if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY
// 如果 store 是从 props 中来的,就不再传入 subscription 实例,否则使用 context 中传入的 subscription 实例
const subscription = new Subscription(
store,
didStoreComeFromProps ? null : contextValue.subscription
)
const notifyNestedSubs = subscription.notifyNestedSubs.bind(subscription)
return [subscription, notifyNestedSubs]
}, [store, didStoreComeFromProps, contextValue])
// contextValue 就是 store,将 store 重新覆盖一遍,注入 subscription,这样被 connect 的组件在 context 中可以拿到 subscription
const overriddenContextValue = useMemo(() => {if (didStoreComeFromProps) {
// 如果组件是直接订阅到来自 props 中的 store,就直接使用来自 props 中的 context
return contextValue
}
// Otherwise, put this component's subscription instance into context, so that
// connected descendants won't update until after this component is done
// 意译:// 如果 store 是从 context 获取的,那么将 subscription 放入上下文,// 为了保证在 component 更新完毕之前被 connect 的子组件不会更新
return {
...contextValue,
subscription
}
}, [didStoreComeFromProps, contextValue, subscription])
// 内置 reducer,来使组件更新,在 checkForUpdates 函数中会用到,作为更新机制的核心
const [[previousStateUpdateResult],
forceComponentUpdateDispatch
] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)
if (previousStateUpdateResult && previousStateUpdateResult.error) {throw previousStateUpdateResult.error}
// Set up refs to coordinate values between the subscription effect and the render logic
/*
* 官方解释:* useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。* 返回的 ref 对象在组件的整个生命周期内保持不变。*
* ref 不仅用于 DOM,useRef()的 current 属性可以用来保存值,类似于类的实例属性
*
* */
const lastChildProps = useRef() // 组件的 props,包括来自父级的,store,dispatch
const lastWrapperProps = useRef(wrapperProps) // 组件本身来自父组件的 props
const childPropsFromStoreUpdate = useRef() // 标记来自 store 的 props 是否被更新了
const renderIsScheduled = useRef(false) // 标记更新的时机
/*
* actualChildProps 是真正要注入到组件中的 props
* */
const actualChildProps = usePureOnlyMemo(() => {
// Tricky logic here:
// - This render may have been triggered by a Redux store update that produced new child props
// - However, we may have gotten new wrapper props after that
// If we have new child props, and the same wrapper props, we know we should use the new child props as-is.
// But, if we have new wrapper props, those might change the child props, so we have to recalculate things.
// So, we'll use the child props from store update only if the wrapper props are the same as last time.
/*
* 意译:* 这个渲染将会在 store 的更新产生新的 props 时候被触发,然而,我们可能会在这之后接收到来自父组件的新的 props,如果有新的 props,* 并且来自父组件的 props 不变,我们应该依据新的 child props 来更新。但是来自父组件的 props 更新也会导致整体 props 的改变,不得不重新计算。* 所以只在新的 props 改变并且来自父组件的 props 和上次一致(下边代码中的判断条件成立)的情况下,才去更新
*
* 也就是说只依赖于 store 变动引起的 props 更新来重新渲染
* */
if (
childPropsFromStoreUpdate.current &&
wrapperProps === lastWrapperProps.current
) {return childPropsFromStoreUpdate.current}
return childPropsSelector(store.getState(), wrapperProps)
}, [store, previousStateUpdateResult, wrapperProps])
// We need this to execute synchronously every time we re-render. However, React warns
// about useLayoutEffect in SSR, so we try to detect environment and fall back to
// just useEffect instead to avoid the warning, since neither will run anyway.
/*
* 意译:我们需要在每次重新渲染的时候同步执行这个 effect。但是 react 将会在 SSR 的情况放下对于 useLayoutEffect 做出警告,* 所以 useIsomorphicLayoutEffect 的最终结果是通过环境判断得出的 useEffect 或 useLayoutEffect。在服务端渲染的时候使用 useEffect,* 因为在这种情况下 useEffect 会等到 js 接管页面以后再去执行,所以就不会 warning 了
* */
/*
* 整体看上下有两个 useIsomorphicLayoutEffect,不同之处在于它们两个的执行时机。*
* 第一个没有传入依赖项数组,所以 effect 会在每次重新渲染的时候执行,负责每次重新渲染的
* 时候检查来自 store 的数据有没有变化,变化就通知 listeners 去更新
*
* 第二个依赖于 store, subscription, childPropsSelector。所以在这三个变化的时候,去执行 effect。* 其内部的 effect 做的事情有别于第一个,负责定义更新函数 checkForUpdates、订阅更新函数,便于在第一个 effect 响应 store 更新的时候,* 可以将更新函数作为 listener 执行,来达到更新页面的目的
*
* */
useIsomorphicLayoutEffect(() => {
lastWrapperProps.current = wrapperProps // 获取到组件自己的 props
lastChildProps.current = actualChildProps // 获取到注入到组件的 props
renderIsScheduled.current = false // 表明已经过了渲染阶段
// If the render was from a store update, clear out that reference and cascade the subscriber update
// 如果来自 store 的 props 更新了,那么通知 listeners 去执行,也就是执行先前被订阅的 this.handleChangeWrapper(Subscription 类中),// handleChangeWrapper 中调用的是 onStateChange,也就是在下边赋值的负责更新页面的函数 checkForUpdates
if (childPropsFromStoreUpdate.current) {
childPropsFromStoreUpdate.current = null
notifyNestedSubs()}
})
// Our re-subscribe logic only runs when the store/subscription setup changes
// 重新订阅仅在 store 内的 subscription 变化时才会执行。这两个变化了,也就意味着要重新订阅,因为保证传递最新的数据,所以之前的订阅已经没有意义了
useIsomorphicLayoutEffect(() => {
// 如果没有订阅,直接 return,shouldHandleStateChanges 默认为 true,所以默认情况会继续执行
if (!shouldHandleStateChanges) return
// Capture values for checking if and when this component unmounts
// 当组件卸载的时候,用闭包,声明两个变量标记是否被取消订阅和错误对象
let didUnsubscribe = false
let lastThrownError = null
// 当 store 或者 subscription 变化的时候,回调会被重新执行,从而实现重新订阅
const checkForUpdates = () => {if (didUnsubscribe) {
// 如果取消订阅了,那啥都不做
return
}
// 获取到最新的 state
const latestStoreState = store.getState()
let newChildProps, error
try {
// 使用 selector 获取到最新的 props
newChildProps = childPropsSelector(
latestStoreState,
lastWrapperProps.current
)
} catch (e) {
error = e
lastThrownError = e
}
if (!error) {lastThrownError = null}
// 如果 props 没变化,只通知一下 listeners 更新
if (newChildProps === lastChildProps.current) {
/*
* 浏览器环境下,useLayoutEffect 的执行时机是 DOM 变更之后,绘制之前。* 由于上边的 useIsomorphicLayoutEffect 在这个时机执行将 renderIsScheduled.current 设置为 false,* 所以会走到判断内部,保证在正确的时机触发更新
*
* */
if (!renderIsScheduled.current) {notifyNestedSubs()
}
} else {
/*
* 如果 props 有变化,将新的 props 缓存起来,并且将 childPropsFromStoreUpdate.current 设置为新的 props,便于在第一个
* useIsomorphicLayoutEffect 执行的时候能够识别出 props 确实是更新了
* */
lastChildProps.current = newChildProps
childPropsFromStoreUpdate.current = newChildProps
renderIsScheduled.current = true
// 当 dispatch 内置的 action 时候,ConnectFunction 这个组件会更新,从而达到更新组件的目的
forceComponentUpdateDispatch({
type: 'STORE_UPDATED',
payload: {
latestStoreState,
error
}
})
}
}
// onStateChange 的角色也就是 listener。在 provider 中,赋值为更新 listeners。在 ConnectFunction 中赋值为 checkForUpdates
// 而 checkForUpdates 做的工作就是根据 props 的变化,相当于 listener,更新 ConnectFunction 自身
subscription.onStateChange = checkForUpdates
subscription.trySubscribe()
// 第一次渲染后先执行一次,从 store 中同步数据
checkForUpdates()
// 返回一个取消订阅的函数,目的是在组件卸载时取消订阅
const unsubscribeWrapper = () => {
didUnsubscribe = true
subscription.tryUnsubscribe()
if (lastThrownError) {throw lastThrownError}
}
return unsubscribeWrapper
}, [store, subscription, childPropsSelector])
// 将组件的 props 注入到我们传入的真实组件中
const renderedWrappedComponent = useMemo(() => <WrappedComponent {...actualChildProps} ref={forwardedRef} />,
[forwardedRef, WrappedComponent, actualChildProps]
)
const renderedChild = useMemo(() => {if (shouldHandleStateChanges) {
// If this component is subscribed to store updates, we need to pass its own
// subscription instance down to our descendants. That means rendering the same
// Context instance, and putting a different value into the context.
/*
* 意译:如果这个组件订阅了 store 的更新,就需要把它自己订阅的实例往下传,也就意味这其自身与其
后代组件都会渲染同一个 Context 实例,只不过可能会向 context 中放入不同的值
再套一层 Provider,将被重写的 context 放入 value。这是什么意思呢?也就是说,有一个被 connect 的组件,又嵌套了一个被 connect 的组件,保证这两个从 context 中获取的 subscription 是同一个,而它们可能都会往 context 中新增加值,我加了一个,我的子组件也加了一个。最终的 context 是所有组件的 value 的整合,而 subscription 始终是同一个
* */
return (<ContextToUse.Provider value={overriddenContextValue}>
{renderedWrappedComponent}
</ContextToUse.Provider>
)
}
// 依赖于接收到的 context,传入的组件,context 的 value 的变化来决定是否重新渲染
return renderedWrappedComponent
}, [ContextToUse, renderedWrappedComponent, overriddenContextValue])
return renderedChild
}
// 根据 pure 决定渲染逻辑
const Connect = pure ? React.memo(ConnectFunction) : ConnectFunction
// 添加组件名
Connect.WrappedComponent = WrappedComponent
Connect.displayName = displayName
// 如果 forwardRef 为 true,将 ref 注入到 Connect 组件,便于获取到组件的 DOM 实例
if (forwardRef) {
const forwarded = React.forwardRef(function forwardConnectRef(
props,
ref
) {return <Connect {...props} forwardedRef={ref} />
})
forwarded.displayName = displayName
forwarded.WrappedComponent = WrappedComponent
return hoistStatics(forwarded, WrappedComponent)
}
// 保留组件的静态方法
return hoistStatics(Connect, WrappedComponent)
}
}
看完了源码,我们整体概括一下 React-Redux 中被 connect 的组件的更新机制:
这其中有三个要素必不可少:
- 根据谁变化(store)
- 更新函数(checkForUpdates)
- 将 store 和更新函数建立联系的 Subscription
connectAdvanced 函数内从 context 中获取 store
,再获取subscription
实例(可能来自 context 或新创建),然后创建更新函数 checkForUpdates
,
当组件初始化,或者 store、Subscription 实例、selector 变化的时候,订阅或者重新订阅。在每次组件更新的时候,检查一下 store 是否变化,有变化则通知更新,
实际上执行 checkForUpdates,本质上调用内置 reducer 更新组件。每次更新导致 selector 重新计算,所以组件总是能获取到最新的 props。所以说,更新机制的最底层
是通过 connectAdvanced 内置的 Reducer 来实现的。
总结
至此,围绕常用的功能,React-Redux 的源码就解读完了。回到文章最开始的三个问题:
- Provider 是怎么把 store 放入 context 中的
- 如何将 store 中的 state 和 dispatch(或者调用 dispatch 的函数)注入组件的 props 中的
- 我们都知道在 Redux 中,可以通过 store.subscribe()订阅一个更新页面的函数,来实现 store 变化,更新 UI,而 React-Redux 是如何做到
store 变化,被 connect 的组件也会更新的
现在我们应该可以明白,这三个问题对应着 React-Redux 的三个核心概念:
- Provider 将数据由顶层注入
- Selector 生成组件的 props
- React-Redux 的更新机制
它们协同工作也就是 React-Redux 的运行机制:Provider 将数据放入 context,connect 的时候会从 context 中取出 store,获取 mapStateToProps,mapDispatchToProps,使用 selectorFactory 生成 Selector 作为 props 注入组件。其次订阅 store 的变化,每次更新组件会取到最新的 props。
阅读源码最好的办法是先确定问题,有目的性的去读。开始的时候我就是硬看,越看越懵,换了一种方式后收获了不少,相信你也是。
欢迎关注我的公众号:一口一个前端,不定期分享我所理解的前端知识