react-redux 源码解读
[TOC]
前置知识
阅读本篇文章前,请先确认你是否了解以下知识:
- react
- redux
- 高阶组件
- react diff 机制
其中高阶组件如果你不太了解也无所谓,你只需要知道,高阶组件就是一个工厂函数,它接收一个组件类(或者函数组件),返回一个被修改后的新的组件类。connect
就是一个高阶组件。
文章内会使用的简写
-
hoc
: 高阶组件(higher order component) -
scu
: shouldComponentUpdate
Issues
我们知道,react-redux 为开发者提供了 redux 到 react 的 binding。本文并不逐行地对源码进行细致分析,不如说是基于以下几个问题,对源码进行大致的扫览。
我们把关注点放在:
- connect 是通过什么方式来连接 store 的?
- 怎么分发 state tree 到子组件的?
provider 仅仅只是保持了一个 store 实例,即使 store 中的 state tree 变化了,由于 react diff 阶段只做浅比较,仅比较对象引用,故 provider.props.store 被视为未发生变化,那就无法把新的 state tree 分发到子组件了。
我们知道 connect 还接收若干配置函数,用来 mapXxToProps
,以及设置是否进行 pure
优化等。这些没什么意思,我们就不 care 了。
两个核心 api
react-redux 只有两个核心的部分
Provider
connect
Provider
Provider
的作用仅仅是维护一个 store
实例,并使用 context
, 为分发 state tree 提供了机制
connect
connect
顾名思义,连接 container 和 store ,使得 container 能响应 state tree 的变化。
connect
本身是一个高阶组件,它调用了 connectAdvanced
这么一个工厂函数。
而上面提到的两个问题,全都由 connectAdvanced
封装了。
connectAdvanced
connectAdvanced
是一个高阶组件的工厂函数,它返回一个 hoc (高阶组件),这个 hoc 最终返回一个 Connect(WrappedComponent)
的组件。
connect 是通过什么方式来连接 store 的?
在这个 Connect(WrappedComponent)
中,声明了 getChildContext
函数,通过它来获取到 Provider 中共享的 store 实例。
NOTE : 理论上,Provider 下任何一个子组件,只要我们也去声明了 getChildContext
,那么它就和 connect
了一样,可以得到 store 实例了。
connect 怎么分发 state tree 到子组件的?
一句话,主要通过 store.subscribe
接口和组件 props 变更自动 re-render 机制。
在 react-redux 中,我们定义了这么几个东西,帮助我们来做到上面几件事情。
- selector
- subscriptions
每个 Connect(WrappedComponent)
在构造函数中都需要 init 上述两个实体。
selector
selector
主要由 selectorFactory
构造,selector
内部包含
- 一个计算新 props 的生成函数
sourceSelector(store.getState(), props)
其中props
是 selector 之前缓存的,后续会直接拿来和store.getState()
比较引用 - 一个 shouldComponentUpdate 的标识
在Connect(WrappedComponent)
内部是直接使用它来判断 scu 的
shouldComponentUpdate() {
return this.selector.shouldComponentUpdate
}
源码如下
const selector = {
// 实际 run 的逻辑又是在 SelectorFactory 里定义的,这里只是做 scu
run: function runComponentSelector(props) {
try {
const nextProps = sourceSelector(store.getState(), props)
// connect 的 scu 实际逻辑在这里定义了
// 比较 run 之后两次 props 的引用是否仍然保持相等
if (nextProps !== selector.props || selector.error) {
selector.shouldComponentUpdate = true
selector.props = nextProps
selector.error = null
}
} catch (error) {
selector.shouldComponentUpdate = true
selector.error = error
}
}
}
调用 selector.run
的时候,主要做了两件事情
- 根据之前缓存的 prevProps 和当前 redux store 实例中的 statet tree 生成新的 props。
- 通过比较引用,更新 shouldComponentUpdate 判断标识
Connect(WrappedComponent)
会在三处调用 selector.run
:
- componentDidMount
- componentWillReceiveProps
- onStateChanged
之后 React 就会自动向子组件分发变更后的 props ,实现 re-render。
subcription
我们在上面提到过 Provider 本身是无法通知 state tree 的变化的,于是为了监听 state tree 变化,我们需要通过 store.subcribe 接口,来向 Provider 的 store 实例注册一些监听器。
我们已经知道,redux 中,store.subcribe 允许用户注册监听器,这些监听器会在每次 dispatch 执行结束后遍历触发。
于是在 react-redux 中, Connect(WrappedComponent)
中的 subcription 最终就是被注册到 store 中。
initSubscription() {
// ...省略一些无关代码
this.subscription = new Subscription(this.store, parentSub, this.onStateChange.bind(this))
}
其中,
- parentSub 是为了在嵌套的 connect 中嵌套执行 subscription。
-
new Subscription
主要构造了一个包含trySubscribe
方法的对象
trySubscribe() {
if (!this.unsubscribe) {
this.unsubscribe = this.parentSub
? this.parentSub.addNestedSub(this.onStateChange)
: this.store.subscribe(this.onStateChange)
this.listeners = createListenerCollection()
}
}
Connect(WrappedComponent)
在 componentDidMount
时 trySubscribe
,在 componentWillUnmount
时 tryUnsubscribe
而 trySubscribe 的主要逻辑就是将 onStateChange
注册到 redux.store 中。
而我们已经说过,onStateChange 主要就是执行 selector.run
onStateChange() {
// 得到新的 props 和 state
this.selector.run(this.props)
if (!this.selector.shouldComponentUpdate) {
this.notifyNestedSubs()
} else {
this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate
this.setState(dummyState)
}
}
于是,每次 state tree 发生变化后,或者更准确地说,每次 dispatch 成功后,redux.store 都会通过注册好的 subscription,执行 Connect(WrappedComponent)
中的 onStateChange
,再由 selector.run
来判断是否需要生成新的 props。