前言
前面学习redux、redux-thunk
的源码,接下来再接再厉,来了解一下react-redux
的源码。
p.s.这里研究的源码版本号是:7.2.4
作用
官方文档有这么一段话,翻译过来的意思是:redux
是独立的库,它可以用于多种库或者框架中,我们将它与某种UI库(框架)结合使用的时候,比起在UI库中操作redux
,我们往往会使用一种UI binding
库去作为它们之间的桥梁。
Redux itself is a standalone library that can be used with any UI layer or framework, including React, Angular, Vue, Ember, and vanilla JS. Although Redux and React are commonly used together, they are independent of each other.
If you are using Redux with any kind of UI framework, you will normally use a "UI binding" library to tie Redux together with your UI framework, rather than directly interacting with the store from your UI code.
它的主要作用有:
react-redux
还是redux
官方团队维护的,当redux
更新时,这个库也会跟着更新;- 提高了性能,通常情况下,当一个
component
发生改变时,整棵树都会重新渲染一遍,但是react-redux
内部帮我们做了优化,提高了性能; - 社区强大。
源码分析
源码src/index.js
文件中暴露出来的有:
export {
Provider,
connectAdvanced,
ReactReduxContext,
connect,
batch,
useDispatch,
createDispatchHook,
useSelector,
createSelectorHook,
useStore,
createStoreHook,
shallowEqual,
}
ReactReduxContext
这个源码很简单,就是使用React.createContext
创建了一个全局上下文。所以react-redux
本质上就是借助context
,需要借助Context
给嵌套的组件传递store
, 还需要当store
中的state
更新时,让嵌套的子组件的state
也更新,所以后期会在context
中保存store
和description
,简化了redux
使用。
// src/components/Context.js
import React from 'react'
export const ReactReduxContext = /*#__PURE__*/ React.createContext(null)
if (process.env.NODE_ENV !== 'production') {
ReactReduxContext.displayName = 'ReactRedux'
}
export default ReactReduxContext
batch
这是一个变量,跟react
中的批量更新有关。批量更新,按照我现阶段的接触是这么理解的:若是连续调用N次更新,react
不会每次都触发视图重渲染,而是会等更新都直接完获得最终结果的时候才会更新视图。
// src/utils/reactBatchedUpdates
export { unstable_batchedUpdates } from 'react-dom'
// src/index.js
import { unstable_batchedUpdates as batch } from './utils/reactBatchedUpdates'
Provider
Provider
的作用就是,你可以在父级组件中将store
的状态注入,然后父组件包裹的任意组件都可以使用到store
中的状态,而不用每个文件都要手动引用store
。
ReactDOM.render(
<Provider store={store}>
<App />
</Provider >,
document.getElementById('root')
);
Provider
是一个组件,那么基本结构跟普通的组件无异,根据文档可知,它接受三个参数:store
、chidren(一般是我们的App组件)
、context
,(如果这样提供了context
的话,还需要为所有连接的组件提供相同的context
)。
上文我们知道了react-redux
主要是运用到了context
,这里我们不难分析出Provider
的基本架构:
import { ReactReduxContext } from './Context'
function Provider({store, context, children}){
const Context = context || ReactReduxContext;
return (
<Context.Provider value={store}>{children}</Context.Provider>
)
}
在Provider
中我们需要对Context
和store
来做一些处理,在此之前,我们先来分析几个小方法:
createListenerCollection
从函数名字来看,就是创建一个listener
收集器,主要作用就是创建链表,每个节点有callback
回调函数和双指针:前一个节点的指针(prev
)和下一个节点的指针(next
)。
createListenerCollection
内声明一个first
指向这个链表的头部,last
指向这个链表的尾巴,还定义了以下几个方法:
clear
: 清除first
和last
指针,即清空链表;notify
:遍历listener
中的每个节点,调用他们的callback
。这里还使用到了上面提到的batch
。猜测是遍历节点调用函数,batch
可以确保函数都调用完成之后再一次性更新,达到批处理的作用,而不是每执行一次回调函数,就更新一次;get
:遍历整个链表,将每个节点依次保存在listeners
数组中,并返回;subscribe
: 新增监听的节点,先设置isSubscribed = true
,表明已经监听,然后像链表新增节点一样新增节点:- 创建一个新节点,含有
callback、prev和next
属性, 其中prev
赋值为last
(因为新节点会放在最后一个节点后面嘛); - 判断现在的链表是不是空的,如果是空的,那么这个节点就是头节点,否则,就将当前节点的上一个节点的
next
指向这个节点。
最后还会返回一个取消监听的函数,取消监听这个节点:
- 如果
isSubscribed
为false
或者first
(链表)为空,就直接返回; - 设置
isSubscribed
为false
; 删除逻辑可能有点绕,我们举个例子,当前节点是B:
- 如果
B
有下一个节点C
,那么我们将C
的prev
指向B
的上一个节点; - 否则,即
B
没有下一个节点,那么说明此时last
指向的是B
,那么应该将last
指向B
的上一个节点; - 如果
B
有上一个节点A
, 那么A
的next
应该指向B
的下一个节点; - 否则,说明
B
即在链表头,此时first
指向的就是B
,应该修改为B
的下一个节点。
- 如果
- 创建一个新节点,含有
import { getBatch } from './batch'
function createListenerCollection() {
// batch是批处理,react的更新机制跟batch有关,当有多个值一起更新时,会将他们合并起来一次更新,而不是每次更新一个值就重新渲染
const batch = getBatch()
let first = null // 头指针
let last = null // 尾指针, 说明这个时双向链表
return {
// 清空指针,即链表清空
clear() {
first = null
last = null
},
notify() {
batch(() => { // 进行批处理调用了回调函数
let listener = first // 从头遍历链表,执行每个回调函数
while (listener) {
listener.callback()
listener = listener.next
}
})
},
get() {
let listeners = []
let listener = first
while (listener) {
listeners.push(listener)
listener = listener.next
}
return listeners // 返回所有订阅的回调函数
},
subscribe(callback) {
let isSubscribed = true // 表示已订阅
let listener = (last = { // listener是一个节点,含有双向指针
callback,
next: null, // 新增的节点,尾巴是空节点,所以是null
prev: last, // 新增节点的前一个节点,肯定是上一个节点
})
if (listener.prev) {
// 如果新增的节点有前一个节点的话,那么它上一个节点要用next指针指向它
listener.prev.next = listener
} else {
// 没有的话,说明新增的节点就算第一个节点
first = listener
}
return function unsubscribe() {
// 如果没有订阅,或者链表是空的,那么就不用取消监听啦
if (!isSubscribed || first === null) return
// 取消订阅
isSubscribed = false
// 由于删除了该节点,需要修改它的前后节点的指针指向
// 也许还需要修改first和last指针
if (listener.next) {
listener.next.prev = listener.prev
} else {
last = listener.prev
}
if (listener.prev) {
listener.prev.next = listener.next
} else {
first = listener.next
}
}
},
}
}
Subscription
Provider
将store
中的state
传入到各个组件中并在state
更新的时候同时让各个组件中的store
也发生改变,那么需要实时监听store
并且支持解除绑定监听的操作。所以内部封装一个名为Subscription
的函数,这个函数的意思是订阅。这个函数后期在其他方法实现中也会用到,所以需要理清它的思路。
encapsulates the subscription logic for connecting a component to the redux store, as well as nesting subscriptions of descendant components, so that we can ensure the ancestor components re-render before descendants
封装用于将组件连接到redux存储的订阅逻辑,以及对后代组件的嵌套订阅,以便我们能够确保祖先组件在后代组件之前re-render
// src/utils/Subscription.js
const nullListeners = { notify() {} }
export default class Subscription {
constructor(store, parentSub) {
this.store = store // redux中的store
this.parentSub = parentSub // context.store
this.unsubscribe = null // 订阅对象
this.listeners = nullListeners // 订阅回调函数
this.handleChangeWrapper = this.handleChangeWrapper.bind(this)
}
// 给嵌套的组件增加监听
addNestedSub(listener) {
this.trySubscribe()
return this.listeners.subscribe(listener)
}
// 通知嵌套组件state发生了改变
notifyNestedSubs() {
this.listeners.notify()
}
// 当store改变时执行的回调函数
handleChangeWrapper() {
if (this.onStateChange) {
this.onStateChange()
}
}
// 判断是否订阅了
isSubscribed() {
return Boolean(this.unsubscribe)
}
// 新增订阅
// 如果没有订阅,那么进行订阅,其中parentSub的优先级比store高
trySubscribe() {
if (!this.unsubscribe) {
this.unsubscribe = this.parentSub
? this.parentSub.addNestedSub(this.handleChangeWrapper)
: this.store.subscribe(this.handleChangeWrapper)
// 初始化listeners, 创建链表
this.listeners = createListenerCollection()
}
}
// 取消订阅
tryUnsubscribe() {
if (this.unsubscribe) {
this.unsubscribe()
this.unsubscribe = null
this.listeners.clear()
this.listeners = nullListeners
}
}
}
综上所述,Subscription
的作用就是传入store
并创建一个listeners
对象,来对store
进行监听并通知各个子组件,或者传入parentSub
,新增监听节点,并遍历
useIsomorphicLayoutEffect
在研究useIsomorphicLayoutEffect
之前,我们先来捋一下useEffect
和useLayoutEffect
的区别:
useEffect
: 当render
结束之后,callback
函数会被执行,由于是异步的,所以不会阻塞浏览器的渲染;useLayoutEffect
: 如果你在useEffect
中需要处理DOM
操作,可以使用useLayoutEffect
,不会出现闪屏的情况。它会在DOM
完成后立即执行,但是会在浏览器进行任何绘制之前执行,所以会阻塞浏览器的渲染,是同步渲染;- 无论
useLayoutEffect
还是useEffect
都无法在Javascript
代码加载完成之前执行。在服务端渲染组件中引入useLayoutEffect
代码时会触发React
警告。解决这个问题,需要将代码逻辑移至useEffect
中。
Provider
中
import { useEffect, useLayoutEffect } from 'react'
export const useIsomorphicLayoutEffect =
typeof window !== 'undefined' &&
typeof window.document !== 'undefined' &&
typeof window.document.createElement !== 'undefined'
? useLayoutEffect
: useEffect
为了解决上面的问题,浏览器渲染使用的是useLayoutEffect
,服务器端渲染使用的是useEffect
。使用useLayoutEffect
是为了确保store
的订阅回调函数的执行发生在最后一次的render commit
,否则当store
的更新发生在render
和effect
之间时,我们可能会丢失更新。
我们也需要确保subscription
创建的时候是异步的,否则当store
的更新发生在subscription
的创建前,我们观察到的state
可能与store
中的不一致。
源码
import React, { useMemo } from 'react'
import PropTypes from 'prop-types'
import { ReactReduxContext } from './Context'
import Subscription from '../utils/Subscription'
import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'
function Provider({ store, context, children }) {
// useMemo会在依赖项发生改变的时候,重新调用内部的函数
// 所以当store发生改变的时候,会重新计算contextValue的值
const contextValue = useMemo(() => {
// 创建Subscription对象
const subscription = new Subscription(store)
// 将notifyNestedSubs绑定在onStateChange
subscription.onStateChange = subscription.notifyNestedSubs
// 敲重点,在react-redux中,contextValue的值都含有这两个属性
// subscription是一个监听对象,当store发生改变时,可以通过subscription
// 给订阅了store的组件传递信息、也可以取消监听
return {
store,
subscription,
}
}, [store])
// 当store发生改变的时候,重新计算当前的getState的值
const previousState = useMemo(() => store.getState(), [store])
// 当previousState或者contextValue发生改变的时候触发下面的逻辑
useIsomorphicLayoutEffect(() => {
const { subscription } = contextValue
// 遍历链表的每个节点,触发回调函数,给订阅了store的组件发布通知
// 之后创建一个新的链表
subscription.trySubscribe()
// 可能发布通知之后state的值发生改变,
// 继续遍历链表,通知
if (previousState !== store.getState()) {
subscription.notifyNestedSubs()
}
return () => {
// 取消监听,便于垃圾回收机制回收
subscription.tryUnsubscribe()
subscription.onStateChange = null
}
}, [contextValue, previousState])
// 如果我们没传递context,就默认使用ReactReduxContext
const Context = context || ReactReduxContext
return <Context.Provider value={contextValue}>{children}</Context.Provider>
}
export default Provider
从源码可以看出来Provider
是一个函数,即可以充当组件,内部主要执行了几个步骤:
- 给
store
创建了一个subscription
对象,便于store
更新的时候借助subscription
进行通知,并将两个赋值给ContextValue
; - 使用
Context.Provider
将ContextValue
传递给Children
。
这里可以抛出一个疑问,如果react-redux中是借助context来传递store的话,那么如果同时要支持自定义的context的话,内部做了什么处理?
当在Provider
中使用Context
时候,先创建一个Conetxt
,将其作为参数传入Provider
中,但是一定要在子组件中手动引入这个Context
不然就会报错:
import React from 'react';
export const ColorContext = React.createContext({color: 'red'});
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import store from './store';
import { Provider } from 'react-redux';
import { ColorContext } from './store/context';
ReactDOM.render(
<Provider store={store} context={ColorContext}>
<App />
</Provider >,
document.getElementById('root')
);
为了避免错误,在子组件中,context
作为props
传入:
// App.js
<Count context={ColorContext} />
为了在内部读取到ColorContext
,可以再ownProps
中获取:
// Counter.js
function Counter(props){
const { count, increment, ColorContext } = props;
const contextValue = useContext(ColorContext);
console.log('contextValue123', contextValue);
return (
<>
<p>{count}</p>
<button onClick = { () => increment(1)}>+</button>
</>
)
}
function mapStateToProps(state, ownProps) {
return {
count: state.count,
ColorContext: ownProps.ownProps
}
}
function mapDispatchToProps(dispatch) {
return { increment: bindActionCreators(actionCreators.increment, dispatch) }
}
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
另外根据后面的源码可得, 这样也不会报错...,但是无法在ownProps
获得Context
<Count store={store} />
connectAdvanced
这是一个从来没有用过的API
!connect
的底层是靠它实现的,不过官方文档说,未来可能会将它从代码中移除。connectAdvanced
的跟connect
的区别在于,并没有规定如何将state、props和dispatch
传入到最终的props
,也没有对产生的props
做缓存来优化性能。
connectAdvanced
接收两个参数:selectorFactory
和factoryOptions
。在selectorFactory
我们可以实现将什么样的参数传入到组件中,这些参数可以依赖于store.getState()
、action
和组件自身的props
。官方DEMO
:
function selectorFactory(dispatch) {
let ownProps = {}
let result = {}
const actions = bindActionCreators(actionCreators, dispatch)
const addTodo = (text) => actions.addTodo(ownProps.userId, text)
return (nextState, nextOwnProps) => {
const todos = nextState.todos[nextOwnProps.userId]
const nextResult = { ...nextOwnProps, todos, addTodo }
ownProps = nextOwnProps
if (!shallowEqual(result, nextResult)) result = nextResult
return result
}
}
export default connectAdvanced(selectorFactory)(TodoApp)
我们姑且将selectorFactory
中返回的函数叫做selector
吧。selector
函数接收两个参数,新的state
和组件自身的props
。selector
函数会在组件接收到新的state
或者props
时被调用,然后返回一个简单对象给被包裹的组件。
而factoryOptions
中的参数可以有这几种:
getDisplayName
: 获得被connectAdvanced
包裹后返回的新组件的名字,默认是ConnectAdvanced('+name+')
methodName
: 用来显示在错误信息中的,默认是connectAdvanced
;shouldHandleStateChanges
: 是否要跟踪state
的变化,默认值是true
;forwardRef
: 当需要使用ref
来保存被connectAdvanced
包裹之后的新组件时,要设置为false
;context
:createContext
创建的context
;- 其余参数: 会被传递给
selectorFactory
即selector
方法的第二个参数中。
这里上一下源码的基本架构,有些即将要被移除的参数被我删掉了!
export default function connectAdvanced(
selectorFactory,
{
getDisplayName = (name) => `ConnectAdvanced(${name})`,
methodName = 'connectAdvanced',
shouldHandleStateChanges = true,
forwardRef = false,
context = ReactReduxContext,
// 剩余的参数会传递给selectorFactory
...connectOptions
}
){
// ...
return function wrapWithConnect(WrappedComponent) {
// ...
return hoistStatics(Connect, WrappedComponent)
}
}
hoistStatics
来源于hoist-non-react-statics
这个包,它可以将你原先组件中的静态属性复制给connectAdvanced
包裹后的新组件。举个栗子:
const Component.staticMethod = () => {}
const WrapperComponent = enhance(Component);
typeof WrapperComponent.staticMethod === undefined // true
// 解决方法如下
function enhance(Component) {
class WrapperComponent extends React.Component {/*...*/}
WrapperComponent.staticMethod = Component.staticMethod;
return Enhance;
}
// 当时若我们不知道原组件的所有静态属性值,上面的方法便无效了,
// 所以可以借助hoist-non-react-statics
import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(Component) {
Class WrapperComponent extends React.Component {/* ... */}
hoistNonReactStatic(WrapperComponent, Component);
return WrapperComponent;
}
继续分析,来理解一下内部的每行代码:
export default function connectAdvanced(){
// 这里的前半部分,都对即将移除的属性做报错...
const Context = context; // 保存要传递到组件内部的context
return function wrapWithConnect(WrappedComponent) {
// ...
}
}
进入到wrapWithConnect
源码:
function wrapWithConnect(WrappedComponent) {
// 先获得组件的名字
const wrappedComponentName =
WrappedComponent.displayName || WrappedComponent.name || 'Component'
// 给被包裹之后返回的新组件的起名字 (name) => `ConnectAdvanced(${name})`
const displayName = getDisplayName(wrappedComponentName)
const selectorFactoryOptions = {
...connectOptions,
getDisplayName,
methodName,
shouldHandleStateChanges,
displayName,
wrappedComponentName,
WrappedComponent,
}
const { pure } = connectOptions
// 创建我们的selector函数
function createChildSelector(store) {
return selectorFactory(store.dispatch, selectorFactoryOptions)
}
// 根据pure来判断是否用useMemo来提高被包裹返回的新组件的性能
const usePureOnlyMemo = pure ? useMemo : (callback) => callback()
function ConnectFunction(props){}
// 根据pure来判断是否用React.memo来提高被包裹返回的新组件的性能
const Connect = pure ? React.memo(ConnectFunction) : ConnectFunction
Connect.WrappedComponent = WrappedComponent
Connect.displayName = ConnectFunction.displayName = displayName
// ...
}
这里有一个ConnectFunction
函数,源码巨长,继续分析:
function ConnectFunction(props) {
// 从props中解构出了context、ref和剩余的Props
const [
propsContext,
reactReduxForwardedRef,
wrapperProps,
] = useMemo(() => {
const { reactReduxForwardedRef, ...wrapperProps } = props
return [props.context, reactReduxForwardedRef, wrapperProps]
}, [props])
// 之前有 context = ReactReduxContext; const Context = context
// Provider中用户可以传入自定义的Context,如果没有就默认是ReactReduxContext
// 用户如果之前在Provider传入context,需要在嵌套组件中手动传入该Context,故在这里我们需要对Context的值进行判断,
// 使用合适的Context
const ContextToUse = useMemo(() => {
return propsContext &&
propsContext.Consumer &&
isContextConsumer(<propsContext.Consumer />)
? propsContext
: Context
}, [propsContext, Context])
// 获得上下文中的值
const contextValue = useContext(ContextToUse)
// 前面讲Provider的时候提到过了
// Provider中封装了contextValue, 内部含有store和subscription
// 如果用户传了自定义的Context,嵌套组件必须传通过Context或者是store才不会报错。
// 这里要判断contextValue.store的原因就是
// 如果没有store,则需要从contextValue中找store,如果contextValue中也没有
// 就会报错
// 所以这就是为什么如果用户传递了context, 那么需要在嵌套组件中自定义传入context或者store的原因
const didStoreComeFromProps =
Boolean(props.store) &&
Boolean(props.store.getState) &&
Boolean(props.store.dispatch)
const didStoreComeFromContext =
Boolean(contextValue) && Boolean(contextValue.store)
if (
process.env.NODE_ENV !== 'production' &&
!didStoreComeFromProps &&
!didStoreComeFromContext
) {
throw new Error(
`Could not find "store" in the context of ` +
`"${displayName}". Either wrap the root component in a <Provider>, ` +
`or pass a custom React context provider to <Provider> and the corresponding ` +
`React context consumer to ${displayName} in connect options.`
)
}
// 保存store
const store = didStoreComeFromProps ? props.store : contextValue.store
// 前面提到了,如果store发生改变,那么会调用selector函数
const childPropsSelector = useMemo(() => {
return createChildSelector(store)
}, [store])
// ...
}
const [subscription, notifyNestedSubs] = useMemo(() => {
// 如果不需要监听state的变化,则返回[null, null]
if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY
// 如果store是由context提供的话,需要对contextValue.subscription进行监听
const subscription = new Subscription(
store,
didStoreComeFromProps ? null : contextValue.subscription
)
// 复制notifyNestedSubs
const notifyNestedSubs = subscription.notifyNestedSubs.bind(
subscription
)
return [subscription, notifyNestedSubs]
}, [store, didStoreComeFromProps, contextValue])
// 覆盖当前的contextValue
const overriddenContextValue = useMemo(() => {
// 如果store是来自Props,那么直接返回contextValue
if (didStoreComeFromProps) {
return contextValue
}
// 否则,将该组件的subscription放入上下文中
return {
...contextValue,
subscription,
}
}, [didStoreComeFromProps, contextValue, subscription])
// 当store更新时,需要强制更新被被包裹的组件也进行更新,进而对子组件也进行更新
const [
[previousStateUpdateResult],
forceComponentUpdateDispatch,
] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)
// 当发生错误的时候会抛出异常,正常情况的话previousStateUpdateResult应该是null
if (previousStateUpdateResult && previousStateUpdateResult.error) {
throw previousStateUpdateResult.error
}
// 这里贴一下各个参数的源码
function storeStateUpdatesReducer(state, action) {
const [, updateCount] = state
return [action.payload, updateCount + 1]
}
const EMPTY_ARRAY = []
const initStateUpdates = () => [null, 0]
// 前面提到过,useIsomorphicLayoutEffect内部判断,
// 如果是服务端返回useEffect
// 如果是浏览器端返回useLayoutEffect
import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'
// 这里可以理解成,调用了useEffect/useLayoutEffect,不过参数都是动态的
function useIsomorphicLayoutEffectWithArgs(
effectFunc,
effectArgs,
dependencies
) {
useIsomorphicLayoutEffect(() => effectFunc(...effectArgs), dependencies)
}
// 继续分析源码
const lastChildProps = useRef() // 更新之前传入当前组件的props(Component)
const lastWrapperProps = useRef(wrapperProps) // 更新之前新组件的props (ConnectComponent)
const childPropsFromStoreUpdate = useRef() // 判断组件的更新是否是因为store的更新
const renderIsScheduled = useRef(false)
// usePureOnlyMemo是根据pure来判断是否使用memo
const actualChildProps = usePureOnlyMemo(() => {
// 视图更新可能是由于store更新导致的会让组件 (Component)获得新的props
// 如果Component有新的props,而ConnectComponent的props不变,那么应该使用新的props, 这样我们可以获得ConnectComponent的新props
// 但是如果我们有了新的ConnectComponent props,它可能会改变Component的props,那么就会重新进行计算
// 为了避免出现问题,只有当ConnectComponent的props不变时,我们才会根据Component的新props进行更新
if (
childPropsFromStoreUpdate.current &&
wrapperProps === lastWrapperProps.current
) {
return childPropsFromStoreUpdate.current
}
// 调用selector函数
return childPropsSelector(store.getState(), wrapperProps)
}, [store, previousStateUpdateResult, wrapperProps])
// 我们需要下面的函数来执行同步到渲染,但是在SSR中使用useLayoutEffect会报错,这里我们需要检测一下,如果是ssr,那么就改为使用effect来避免这个警告
useIsomorphicLayoutEffectWithArgs(captureWrapperProps, [
lastWrapperProps,
lastChildProps,
renderIsScheduled,
wrapperProps,
actualChildProps,
childPropsFromStoreUpdate,
notifyNestedSubs,
])
function captureWrapperProps(
lastWrapperProps,
lastChildProps,
renderIsScheduled,
wrapperProps,
actualChildProps,
childPropsFromStoreUpdate,
notifyNestedSubs
) {
// 为了方便下一次的比较
lastWrapperProps.current = wrapperProps
lastChildProps.current = actualChildProps
renderIsScheduled.current = false
// 清空引用并进行更新
if (childPropsFromStoreUpdate.current) {
childPropsFromStoreUpdate.current = null
notifyNestedSubs()
}
}
// 当store或者subscription发生改变时,我们重新订阅
useIsomorphicLayoutEffectWithArgs(
subscribeUpdates,
[
shouldHandleStateChanges,
store,
subscription,
childPropsSelector,
lastWrapperProps,
lastChildProps,
renderIsScheduled,
childPropsFromStoreUpdate,
notifyNestedSubs,
forceComponentUpdateDispatch,
],
[store, subscription, childPropsSelector]
)
function subscribeUpdates(
shouldHandleStateChanges, // 是否对store的更新进行订阅
store, // store
subscription, // subscription
childPropsSelector, // selector
lastWrapperProps, // 上一次组件的props
lastChildProps, // 上一次的子props
renderIsScheduled, // 是否正在调度
childPropsFromStoreUpdate //判断子props是否是来自store的更新
notifyNestedSubs, // notifyNestedSubs
forceComponentUpdateDispatch
) {
// 如果没有订阅,直接返回
if (!shouldHandleStateChanges) return
// 捕获值,并检查是否以及何时卸载该组件
let didUnsubscribe = false
let lastThrownError = null
// 当每次store更新传播到这个组件时,我们就会调用这个函数
const checkForUpdates = () => {
if (didUnsubscribe) {
// 如果已经取消订阅,直接返回
return
}
// 获得更新前的store
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一样,那么就不做任何处理
if (newChildProps === lastChildProps.current) {
if (!renderIsScheduled.current) {
// 调用subscription.notifyNestedSubs()
notifyNestedSubs()
}
} else {
// 用ref来存储最新的props,如果使用useState/useReducer来追踪,
// 那么将无法确定这个值是否会加工过
// 也无法清空这个值触发进行强制渲染
lastChildProps.current = newChildProps
childPropsFromStoreUpdate.current = newChildProps
renderIsScheduled.current = true
// 如果子props被更新了,那么可以重新渲染了
forceComponentUpdateDispatch({
type: 'STORE_UPDATED',
payload: {
error,
},
})
}
}
subscription.onStateChange = checkForUpdates
subscription.trySubscribe()
// 将store中发生改变的值进行视图更新
checkForUpdates()
const unsubscribeWrapper = () => {
didUnsubscribe = true
subscription.tryUnsubscribe()
subscription.onStateChange = null
if (lastThrownError) {
// 当store更新,但是某个组件没有依赖store中更新的state切mapState方法有问题时
// 就会报错
throw lastThrownError
}
}
return unsubscribeWrapper
}
// 回到主要源码中,我们已经处理完了需要传递的参数,接下来就是渲染组件
// 还使用了useMemo来优化性能
const renderedWrappedComponent = useMemo(
() => (
<WrappedComponent
{...actualChildProps}
ref={reactReduxForwardedRef}
/>
),
[reactReduxForwardedRef, WrappedComponent, actualChildProps]
)
// 如果React看到与上次完全相同的元素引用,它会退出重新呈现子元素,就像它被包装在React.memo()中或从shouldComponentUpdate返回false一样。
const renderedChild = useMemo(() => {
if (shouldHandleStateChanges) {
// 如果需要根据store的变化更新组件,那么就使用context.provider封装组件
// 并传入处理好的值
return (
<ContextToUse.Provider value={overriddenContextValue}>
{renderedWrappedComponent}
</ContextToUse.Provider>
)
}
// 否则直接返回原先的组件
return renderedWrappedComponent
}, [ContextToUse, renderedWrappedComponent, overriddenContextValue])
return renderedChild
到这里connectAdvanced
的源码就分析完毕了。我们来捋一下思路,为了方便说明我先写行代码:
function selector = (dispatch, ...args) => {}
const connectComponent = connectAdvanced(selector, options)(component)
connectAdvanced
是一个函数,接收一个selector
方法和options
对象:selector
方法是用户自定义的,接收一个dispacth
参数,最后需要返回一个含有入参为nextState和nextOwnProps
的函数,该内部函数最后会返回一个对象,对象的键值对就是即将传给component
组件的props
;options
对象,可以传递官方指定的,也可以自定义,如果是自定义的,最后会被当做传入selector
的第二个参数中。官方指定的参数中,有几个比较重要,其中pure
的值确定了最后返回的组件是否要进行性能优化;connectAdvanced
最后返回的是一个高阶函数,参数就是要包裹的组件。
- 接下来对
options
中的值进行校验,并且对一些即将移除的属性做出警告处理; - 保存
options
中的context
; - 返回高阶函数
wrapWithConnect(WrappedComponent)
,参数就是要包裹的组件,接下来就是分析wrapWithConnect(WrappedComponent)
中的内容了; - 先对传入的组件进行校验,判断是不是一个合格的组件;
- 记录被包裹的组件
component
的名字,进而给返回的新组件connectComponent
起名字; - 将
options
中多余的参数和定义好的参数封装起来,作为selector
的参数; - 根据
options
的参数pure
,用usePureOnlyMemo
变量来判断是否使用useMemo
方法; 使用
ConnectFunction
创建一个Connect
组件,还给这个组件设置了WrappedComponent
和displayName
,其中这个ConnectFunction
是connectAdvanced
中的核心:ConnectFunction
接受一个参数props
, 这个props
就是被包裹后的ConnetComponent
的参数props
;- 将
context、ref和剩余的参数
从props
中解耦出来; - 获得最终的
context
,并从中取得contextValue
; - 获得
store
; - 定义一个
memo
函数,当store
的值发生改变,就调用用户自定义的selector
函数; - 获得最终的
ContextValue
的值; - 获得最终要返回被包裹的组件的
actualChildProps
: - 当依赖的值发生改变时,使用
useEffect
或者useLayoutEffect
来调用captureWrapperProps
,里面将当前的connentComponent
的props
和store
的state
记录下来,方便下次渲染的时候比较,如果store
的值发生更新,调用notifyNestedSubs
触发每个回调函数; - 当
store
或者subscription
进行更新时,发起通知,subscribeUpdates
; - 最后判断
shouldHandleStateChanges
,来决定是否在外面包裹Context
,不需要则直接返回Connect
组件。
- 获得
Connect
组件后,通过判断forwardRef
,如果存在,我们需要使用React.forwardRef
来转发Connect
,否则直接使用Connect
,还使用了hoistStatics
来合并包裹前后的组件的静态属性。
小结
connectAdvanced
最后的结果是返回一个Context.Comsumer
,其中需要对最后传入组件的props
参数进行处理,connectAdvanced
和connect
最大的区别在于,内部的pure
属性默认是false
,即它不会启动性能优化。
最后回到之前抛出的疑问:如果react-redux中是借助context来传递store的话,那么如果同时要支持自定义的context的话,内部做了什么处理?
react-redux
通过Context
来保存store
和subscription
,subscription
用来进行订阅发布store
的,当需要结合react-redux
搭配使用context
时,记得再嵌套组件中将同个名称的context
当作props
传入组件中,这样connectAdvanced
会将用户自定义的Context
当作props
合并到最终组件的props
中。
所以虽然react-redux
支持你可以全局搞一个context
但是没必要,很鸡肋~
shallowEqual
在研究connect
源码前先学习一下shallowEqual
:
值 | === | is(x,y) |
---|---|---|
NaN、NaN | false | true |
0、+0 | true | true |
0、-0 | true | false |
+0、-0 | true | false |
// Object.is可以对基本数据类型:null,undefined,number,string,boolean做出非常精确的比较,
// 但是对于引用数据类型是没办法直接比较的。
function is(x, y) {
if (x === y) {
// 处理0、+0和-0的情况
return x !== 0 || y !== 0 || 1 / x === 1 / y
} else {
// 处理NaN的情况
return x !== x && y !== y
}
}
export default function shallowEqual(objA, objB) {
// 先用is来判断两个变量是否相等
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
for (let i = 0; i < keysA.length; i++) {
if (
// Object.prototype.hasOwnProperty 和 in 运算符不同,该方法会忽略掉那些从原型链上继承到的属性。
!Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])
) {
return false
}
}
return true
}
一开始先比较,调用了is
来判断两个值是否一致,这里兼容了一些常见的基本来信===
判断有问题的情况;之后判断是否不是对象或者是null
,如果满足就返回false
, 否则就遍历两个对象的key
进行比较。从源码可以看出浅比较是不适用于嵌套类型的比较的。
以上就是浅比较的思想啦~
从MDN可以看出is
是Object.is()
的一种Polyfill
实现。
connect
看源码的时候顺便去看了官方文档,第一个感觉是我的英语真的进步了!第二个感觉是我以前是不是没翻过,怎么看到了很多疏忽的地方??!
基本架构
// src/connect/connect.js
import connectAdvanced from '../components/connectAdvanced'
import shallowEqual from '../utils/shallowEqual'
import defaultMapDispatchToPropsFactories from './mapDispatchToProps'
import defaultMapStateToPropsFactories from './mapStateToProps'
import defaultMergePropsFactories from './mergeProps'
import defaultSelectorFactory from './selectorFactory'
export function createConnect({
onnectHOC = connectAdvanced,
mapStateToPropsFactories = defaultMapStateToPropsFactories,
mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
mergePropsFactories = defaultMergePropsFactories,
selectorFactory = defaultSelectorFactory,
} = {}){
return function connect(
mapStateToProps,
mapDispatchToProps,
mergeProps,
{
pure = true,
areStatesEqual = strictEqual,
areOwnPropsEqual = shallowEqual,
areStatePropsEqual = shallowEqual,
areMergedPropsEqual = shallowEqual,
...extraOptions
} = {}
){
// ...
return connectHOC()
}
}
export default /*#__PURE__*/ createConnect()
导出前执行了createConnect
方法,最后返回的是一个我们认识的connect
函数,内部调用了HOC
函数。这里面涉及到了秘密密麻麻的名字相似的参数,看的有点脑壳疼。
调用createConnect()
时未传递参数,那么里面的参数都是读取默认值:
connectHOC = connectAdvanced,
mapStateToPropsFactories = defaultMapStateToPropsFactories,
mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
mergePropsFactories = defaultMergePropsFactories,
selectorFactory = defaultSelectorFactory,
然后返回了我们属性的connect
函数,它支持四个参数:mapStateToProps、mapDispatch、mergeProps和一个对象
。由官方文档可知:
mapStateToProps
: 函数类型,可以传入store.getState()
和当前组件自身的props
,最后需要返回一个对象;mapDispacthToProps
:函数类型,接收store.dispacth
和当前组件自身的props
,最后需要返回一个对象;mergeProps
: 函数类型,接收store.getState()
、store.dispacth
和组件自身的props
,最后需要返回对象;options
: 这个对象里面有几个值:{ context?: Object, // 当我们在Provider传入context时,在子组件中引入,就可以写在这里 pure?: boolean, // 为了提高react-redux的性能,默认开启pure,类似pureComponent,只有在组件自身的props、mapStateToProps的结果、或者是mapDispatchToProps的结果改变时才会重新渲染组件 areStatesEqual?: Function, // 开启pure时,传递进来的state更新时的比较函数 areOwnPropsEqual?: Function, // 开启pure时,自身props更新时的比较函数 areStatePropsEqual?: Function, // 开启pure时,mapStateToProps更新时的比较函数 areMergedPropsEqual?: Function, // 开启pure时,mergeProps的返回值更新时的比较函数 forwardRef?: boolean, // 如果组件需要接收ref,需要在这里设置为true }
最后返回一个高阶函数,connectHOC
就是我们之前介绍的connectAdvanced
,之前说的connectAdvanced
接收两个参数,一个是selector
函数,一个是options
对象:
return connectHOC(selectorFactory, {
// 为了在报错的时候使用
methodName: 'connect',
// 根据被connect包裹的组件的名字,给返回的新组件起名字
getDisplayName: (name) => `Connect(${name})`,
// 如果mapStateToProps为Null,那么就不需要监听更新state啦
shouldHandleStateChanges: Boolean(mapStateToProps),
// 下面这些值是给selectorFactory做为参数的
// 之前说的connectAdvanced的第二个参数options是个对象
// 里面除了规定的key,其他属性最终都会成为selector函数的第二个参数
initMapStateToProps,
initMapDispatchToProps,
initMergeProps,
pure,
areStatesEqual,
areOwnPropsEqual,
areStatePropsEqual,
areMergedPropsEqual,
// any extra options args can override defaults of connect or connectAdvanced
...extraOptions,
})
逐步认识一下createConnect
中的默认参数:
connectHoc
它的默认值是connectAdvanced
,这个上面已经了解过了,就不重复。
defaultMapStateToPropsFactories
在研究之前来看一下这个源码中引入的文件wrapMapToProps
:
import verifyPlainObject from '../utils/verifyPlainObject'
export function wrapMapToPropsConstant(getConstant) {
return function initConstantSelector(dispatch, options) {
const constant = getConstant(dispatch, options)
function constantSelector() {
return constant
}
// dependsOnOwnProps是判断当前的mapStateToProps的值,有没有依赖于ownProps
constantSelector.dependsOnOwnProps = false
return constantSelector
}
}
// 有没有依赖于ownProps
export function getDependsOnOwnProps(mapToProps) {
return mapToProps.dependsOnOwnProps !== null &&
mapToProps.dependsOnOwnProps !== undefined
? Boolean(mapToProps.dependsOnOwnProps)
: mapToProps.length !== 1
}
// 封装一个mapToProps代理函数
// 检测selectorFactory是否根据ownProps来更新。
export function wrapMapToPropsFunc(mapToProps, methodName) {
return function initProxySelector(dispatch, { displayName }) {
const proxy = function mapToPropsProxy(stateOrDispatch, ownProps) {
return proxy.dependsOnOwnProps
? proxy.mapToProps(stateOrDispatch, ownProps)
: proxy.mapToProps(stateOrDispatch)
}
// 允许detectFactoryAndVerify得到自己的props
proxy.dependsOnOwnProps = true
proxy.mapToProps = function detectFactoryAndVerify(
stateOrDispatch,
ownProps
) {
proxy.mapToProps = mapToProps
proxy.dependsOnOwnProps = getDependsOnOwnProps(mapToProps)
let props = proxy(stateOrDispatch, ownProps)
// props返回函数,则处理mapToProps,并将该新函数作为真正的mapToProps用于后续调用
if (typeof props === 'function') {
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
}
}
// src/connect/mapStateToProps.js
import { wrapMapToPropsConstant, wrapMapToPropsFunc } from './wrapMapToProps'
export function whenMapStateToPropsIsFunction(mapStateToProps) {
return typeof mapStateToProps === 'function'
? wrapMapToPropsFunc(mapStateToProps, 'mapStateToProps')
: undefined
}
export function whenMapStateToPropsIsMissing(mapStateToProps) {
return !mapStateToProps ? wrapMapToPropsConstant(() => ({})) : undefined
}
export default [whenMapStateToPropsIsFunction, whenMapStateToPropsIsMissing]
mapStateToProps
返回一个数组,分别是whenMapStateToPropsIsFunction
和whenMapStateToPropsIsMissing
,从函数名字可得,当mapStateToProps
是函数或者是空的时候,分别调用的这两个函数。
defaultMapDispatchToPropsFactories
// src/connect/mapDispatchToProps.js
import bindActionCreators from '../utils/bindActionCreators'
import { wrapMapToPropsConstant, wrapMapToPropsFunc } from './wrapMapToProps'
export function whenMapDispatchToPropsIsFunction(mapDispatchToProps) {
return typeof mapDispatchToProps === 'function'
? wrapMapToPropsFunc(mapDispatchToProps, 'mapDispatchToProps')
: undefined
}
export function whenMapDispatchToPropsIsMissing(mapDispatchToProps) {
return !mapDispatchToProps
? wrapMapToPropsConstant((dispatch) => ({ dispatch }))
: undefined
}
export function whenMapDispatchToPropsIsObject(mapDispatchToProps) {
return mapDispatchToProps && typeof mapDispatchToProps === 'object'
? wrapMapToPropsConstant((dispatch) =>
bindActionCreators(mapDispatchToProps, dispatch)
)
: undefined
}
export default [
whenMapDispatchToPropsIsFunction,
whenMapDispatchToPropsIsMissing,
whenMapDispatchToPropsIsObject,
]
mapDispatchToProps
返回一个数组,分别是whenMapDispatchToPropsIsFunction
、whenMapDispatchToPropsIsMissing
和whenMapDispatchToPropsIsObject
,从函数名字可得,当mapDispatchToProps
是函数、空或者是对象的时候的时候,分别调用的这三个函数。
defaultMergePropsFactories
// src/connect/mergeProps.js
import verifyPlainObject from '../utils/verifyPlainObject'
export function defaultMergeProps(stateProps, dispatchProps, ownProps) {
return { ...ownProps, ...stateProps, ...dispatchProps }
}
export function wrapMergePropsFunc(mergeProps) {
return function initMergePropsProxy(
dispatch,
{ displayName, pure, areMergedPropsEqual }
) {
let hasRunOnce = false
let mergedProps
return function mergePropsProxy(stateProps, dispatchProps, ownProps) {
const nextMergedProps = mergeProps(stateProps, dispatchProps, ownProps)
if (hasRunOnce) {
if (!pure || !areMergedPropsEqual(nextMergedProps, mergedProps))
mergedProps = nextMergedProps
} else {
hasRunOnce = true
mergedProps = nextMergedProps
if (process.env.NODE_ENV !== 'production')
verifyPlainObject(mergedProps, displayName, 'mergeProps')
}
return mergedProps
}
}
}
export function whenMergePropsIsFunction(mergeProps) {
return typeof mergeProps === 'function'
? wrapMergePropsFunc(mergeProps)
: undefined
}
export function whenMergePropsIsOmitted(mergeProps) {
return !mergeProps ? () => defaultMergeProps : undefined
}
export default [whenMergePropsIsFunction, whenMergePropsIsOmitted]
mergeProps
返回一个数组,分别是whenMergePropsIsFunction
和whenMergePropsIsOmitted
,从函数名字可得,当mergeProps
是函数或者是省略了的时候,分别调用的这两个函数。
defaultSelectorFactory
这个函数的名字翻译过来是: 选择工厂函数,就生成connectAdvanced
需要的selector
函数,selector
的第一个参数是dispatch
, 第二个参数是对象,来瞄一下源码:
// import defaultSelectorFactory from './selectorFactory'
// selectorFactory.js
export default function finalPropsSelectorFactory(
dispatch,
{ initMapStateToProps, initMapDispatchToProps, initMergeProps, ...options }
) {
// 先初始化值
const mapStateToProps = initMapStateToProps(dispatch, options)
const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
const mergeProps = initMergeProps(dispatch, options)
if (process.env.NODE_ENV !== 'production') {
// ...
}
// 根据pure使用不同的函数,并调用它们
const selectorFactory = options.pure
? pureFinalPropsSelectorFactory
: impureFinalPropsSelectorFactory
return selectorFactory(
mapStateToProps,
mapDispatchToProps,
mergeProps,
dispatch,
options
)
}
impureFinalPropsSelectorFactory
如果我们没有开启pure
的话,那么不做什么骚操作,直接返回mergeProps
,输出最后要传递给包裹后的组件的props
:
export function impureFinalPropsSelectorFactory(
mapStateToProps,
mapDispatchToProps,
mergeProps,
dispatch
) {
return function impureFinalPropsSelector(state, ownProps) {
return mergeProps(
mapStateToProps(state, ownProps),
mapDispatchToProps(dispatch, ownProps),
ownProps
)
}
}
pureFinalPropsSelectorFactory
当第一次执行这个函数的时候,会使用存储这次的state、props
,再通过这两个值,去获得最后传给包裹组件的state
和props
合并后的props
;
如果已经执行过的话,调用handleSubsequentCalls
来对比此次更新是只更新state
、只更新props
还是state
和props
都更新,进而调用不同的逻辑,最后给包裹的组件最终的props
。
敲黑板了,下面的逻辑跟react-redux
的性能有关。我们在使用connect
是可以传入mapStateToProps、mapDispatchtoProps、mergeProps和Options
四个参数,其中前两个是必传的。
mapStateToProps
可以传入两个参数,state
和ownProps
,最后返回一个新的state
,所以我们可以知道新的state
不仅取决于store
的变化,也取决于ownProps
的变化,他们两个只要更新一个那么就会重新计算state
;mapDispatchtoProps
也支持传入两个参数,dispatch
和ownProps
,返回一个新的dispatch
,新的dispatch
的更新取决于dispacth
和ownProps
;mergeProps
是一个函数,默认情况下我们都会省略,所以会被默认赋值,最后将新的state
、新的dispacth
和ownProps
变为新的props
传递给Connect
组件。
所以不管是store
的更新还是组件自身props
的更新,都会影响到被connect
返回的新组件,这种性能开销是很大的,所以react-redux
做了性能优化。
connect
比connectAdvanced
多了一个pure
的参数,默认是true
。是用来开启性能优化的。
export function pureFinalPropsSelectorFactory(
mapStateToProps,
mapDispatchToProps,
mergeProps,
dispatch,
{ areStatesEqual, areOwnPropsEqual, areStatePropsEqual }
) {
let hasRunAtLeastOnce = false
let state
let ownProps
let stateProps
let dispatchProps
let mergedProps
// 首次执行,根据当前的state和props,获得最终的stateProps和dispatchProps
// 进而同个mergedProps获得最终传入组件props
function handleFirstCall(firstState, firstOwnProps) {
state = firstState
ownProps = firstOwnProps
stateProps = mapStateToProps(state, ownProps)
dispatchProps = mapDispatchToProps(dispatch, ownProps)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
hasRunAtLeastOnce = true
return mergedProps
}
// 如果自身的Props和store都改变,调用这个函数
function handleNewPropsAndNewState() {
// 那么直接通过mapStateToProps获得新的stateProps
stateProps = mapStateToProps(state, ownProps)
// mapDispatchToProps方法如果有依赖ownProps,那么也需要重新获得新的dispatchProps
if (mapDispatchToProps.dependsOnOwnProps)
dispatchProps = mapDispatchToProps(dispatch, ownProps)
// 传递新的stateProps、dispatchProps,通过mergedProps获得最终传入组件的props
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
return mergedProps
}
// 如果只有自身的props改变
function handleNewProps() {
// 声明mapStateToProps时如果使用了ownProps参数同样会产生新的stateProps!
if (mapStateToProps.dependsOnOwnProps)
stateProps = mapStateToProps(state, ownProps)
// 声明mapDispatchToProps时如果使用了第二个参数(ownProps)这里会标记为true
if (mapDispatchToProps.dependsOnOwnProps)
dispatchProps = mapDispatchToProps(dispatch, ownProps)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
return mergedProps
}
// 只有store改变
function handleNewState() {
// 当store改变,那么mapStateToProps会返回新的值
const nextStateProps = mapStateToProps(state, ownProps)
// 判断新的stateProps和上一次的stateProps是否相等
const statePropsChanged = !areStatePropsEqual(nextStateProps, stateProps)
stateProps = nextStateProps
// 如果改变,才需要重新计算最终的props
if (statePropsChanged)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
return mergedProps
}
// 如果不是第一次执行,
function handleSubsequentCalls(nextState, nextOwnProps) {
// 判断组件自身的Props是否改变
const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps)
// 判断store是否改变
const stateChanged = !areStatesEqual(nextState, state)
// 记录下当前的state和ownProps,方便下次更新比较
state = nextState
ownProps = nextOwnProps
if (propsChanged && stateChanged) return handleNewPropsAndNewState()
if (propsChanged) return handleNewProps()
if (stateChanged) return handleNewState()
return mergedProps
}
return function pureFinalPropsSelector(nextState, nextOwnProps) {
// 判断是否执行过一次,来调用handleSubsequentCalls或者handleFirstCall
return hasRunAtLeastOnce
? handleSubsequentCalls(nextState, nextOwnProps)
: handleFirstCall(nextState, nextOwnProps)
}
}
捋清楚selectorFactory
,接下来就是connectAdvanced
的执行流程了,就不复述了。
Hook
最后来聊一聊新增的Hook
,6月份项目还在啃老本,明明项目用的是hook
,但是关于react-redux
却完全不知道有hook
相关的api
存在,继续学习:
useSelector
useSelector
有点类似于mapStateToProps
,可以从store
中取得我们想要的state
,但是不同的是mapStateToProps
用的是浅比较,而useSelector
用的是深比较,只要state
发生改变,就肯定会触发重新渲染,所以一般情况下,useSelector
只返回一个值,如果想要返回一个对象,建议搭配shallowEqual
进行使用,或者借助reselect
创建一个记忆选择器,该选择器在一个对象中返回多个值,但只在其中一个值发生变化时返回一个新对象。
import { shallowEqual, useSelector } from 'react-redux'
import { createSelector } from 'reselect'
// 常规用法
export const CounterComponent = () => {
const counter = useSelector((state) => state.counter)
return <div>{counter}</div>
}
// 使用浅层比较,获得一个对象
export const ObjComponent = () => {
const obj = useSelector((state) => state.obj, shallowEqual)
return <div>{obj.counter}</div>
}
// 借助reselect
const selectNumCompletedTodos = createSelector(
(state) => state.todos,
(todos) => todos.filter((todo) => todo.completed).length
)
export const CompletedTodosCounter = () => {
const numCompletedTodos = useSelector(selectNumCompletedTodos)
return <div>{numCompletedTodos}</div>
}
export const App = () => {
return (
<>
<span>Number of completed todos:</span>
<CompletedTodosCounter />
</>
)
}
接下来开始分析源码:
// useSelector是由createSelectorHook返回的
export const useSelector = /*#__PURE__*/ createSelectorHook()
所以可以推测出
createSelectorHook() = (selector, equalityFn) => selectedState
来看看createSelectorHook
:
export function createSelectorHook(context = ReactReduxContext) {
// 前面createSelectorHook()没有传递参数,所以context = ReactReduxContext
const useReduxContext =
// 获得context
context === ReactReduxContext
? useDefaultReduxContext
: () => useContext(context)
// 返回一个(selector, equalityFn = refEquality) => selectorState的函数给useSelector
// const refEquality = (a, b) => a === b useSelector默认是严格比较
return function useSelector(selector, equalityFn = refEquality) {
// 对selector, equalityFn进行校验
if (process.env.NODE_ENV !== 'production') {
if (!selector) {
throw new Error(`You must pass a selector to useSelector`)
}
if (typeof selector !== 'function') {
throw new Error(`You must pass a function as a selector to useSelector`)
}
if (typeof equalityFn !== 'function') {
throw new Error(
`You must pass a function as an equality function to useSelector`
)
}
}
// 前面我们看源码的时候知道了,context会存储store和subscription
const { store, subscription: contextSub } = useReduxContext()
// 获得selectedState并返回
const selectedState = useSelectorWithStoreAndSubscription(
selector,
equalityFn,
store,
contextSub
)
useDebugValue(selectedState)
return selectedState
}
}
来看看useSelectorWithStoreAndSubscription
是如何返回我们想要的selectState
:
function useSelectorWithStoreAndSubscription(
selector,
equalityFn,
store,
contextSub // subscription
) {
const [, forceRender] = useReducer((s) => s + 1, 0)
// 创建一个新的订阅对象
const subscription = useMemo(() => new Subscription(store, contextSub), [
store,
contextSub,
])
const latestSubscriptionCallbackError = useRef()
const latestSelector = useRef()
const latestStoreState = useRef()
const latestSelectedState = useRef()
// 获得现在的store中的state
const storeState = store.getState()
let selectedState
try {
// 如果selector改变了、或者store中的state改变了、或者出现错误了
// 那么就重新执行selector获得新的selectState
// 否则直接返回上一次的selectState
if (
selector !== latestSelector.current ||
storeState !== latestStoreState.current ||
latestSubscriptionCallbackError.current
) {
// 将new selectState 和 last selectState比较
// 如果一致,返回上一次的selectState
// 否则,返回new selectState
const newSelectedState = selector(storeState)
if (
latestSelectedState.current === undefined ||
!equalityFn(newSelectedState, latestSelectedState.current)
) {
selectedState = newSelectedState
} else {
selectedState = latestSelectedState.current
}
} else {
selectedState = latestSelectedState.current
}
} catch (err) {
if (latestSubscriptionCallbackError.current) {
err.message += `\nThe error may be correlated with this previous error:\n${latestSubscriptionCallbackError.current.stack}\n\n`
}
throw err
}
// 这个函数每次都被执行的时候,会保存这次的selector、store中的state、selectState和订阅捕获错误
// 便于下次比较做使用
useIsomorphicLayoutEffect(() => {
latestSelector.current = selector
latestStoreState.current = storeState
latestSelectedState.current = selectedState
latestSubscriptionCallbackError.current = undefined
})
// 当store或者subscription发生改变时
// 会调用checkForUpdates获得新的storestate和selectState
// 比较这次和上次的selectState的值
// 如果不等,更新latestSelectedState和latestStoreState、latestSubscriptionCallbackError
useIsomorphicLayoutEffect(() => {
function checkForUpdates() {
try {
const newStoreState = store.getState()
const newSelectedState = latestSelector.current(newStoreState)
if (equalityFn(newSelectedState, latestSelectedState.current)) {
return
}
latestSelectedState.current = newSelectedState
latestStoreState.current = newStoreState
} catch (err) {
// we ignore all errors here, since when the component
// is re-rendered, the selectors are called again, and
// will throw again, if neither props nor store state
// changed
latestSubscriptionCallbackError.current = err
}
forceRender()
}
subscription.onStateChange = checkForUpdates
subscription.trySubscribe()
checkForUpdates()
return () => subscription.tryUnsubscribe()
}, [store, subscription])
// 最后返回我们需要的selectedState
return selectedState
}
useSelector
内部主要通过useSelectorWithStoreAndSubscription
来获得我们想要的selectState
,useSelectorWithStoreAndSubscription
通过比较selector
和store
判断是否要执行selector
来获得最新的selectState
,然后将新获取到了selectState
与上一次selectState
来做比较,判断返回哪一个值。内部为了方便比较,都会记录下当前的selector、store、selectState、和捕获的错误
,目的就是、为了提高useSelector
的性能,不用每次数据明明跟之前一样却要返回新的内容导致视图更新。
useDispatch
useDiapatch
的本质就是获得store
中的dispatch
。举一个官方栗子:
import React, { useCallback } from 'react'
import { useDispatch } from 'react-redux'
export const CounterComponent = ({ value }) => {
const dispatch = useDispatch()
const increaseCounter = useCallback(() => dispatch({ type: 'increase-counter' }), [])
return (
<div>
<span>{value}</span>
<button onClick={increaseCounter}>Increase counter</button>
</div>
)
}
直接看源码:
export function createDispatchHook(context = ReactReduxContext) {
const useStore =
context === ReactReduxContext ? useDefaultStore : createStoreHook(context)
return function useDispatch() {
const store = useStore()
return store.dispatch
}
}
export const useDispatch = /*#__PURE__*/ createDispatchHook()
不难理解,先获得Context
,再获得Context
中的store
,最后返回store.dispatch
。
useStore
在useDispatch
中,有一个代码是createStoreHook(context)
,其实createStoreHook
就是useStore
的实现:
export function createStoreHook(context = ReactReduxContext) {
const useReduxContext =
context === ReactReduxContext
? useDefaultReduxContext
: () => useContext(context)
return function useStore() {
const { store } = useReduxContext()
return store
}
}
export const useStore = /*#__PURE__*/ createStoreHook()
先获得Context
,再获得Context
中的store
,最后将store
返回~
小结
源码断断续续看了一周,还来回看了几遍,有点绕。官网虽然说在不久会废弃connectAdvanced
,但是为了connect
的实现,依旧得好好啃啊。通过学习这次源码也对useMemo
这个钩子加深了印象,知道了mapStateToProps和mapDispacthToProps
可以将ownProps
当作第二个参数传入对性能的影响,最后就是学习到了内部新带hook
,还接触到了reselect
这个库。
最后,一定要多翻翻英文文档啊~
参考
如有错误,欢迎指出,感谢阅读~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。