connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
不用connect
之前有说到过,当dispatch发生之后,所有使用subscribe订阅的listener将会被执行。所以在React项目里可以这么来将组件渲染与store更新绑定
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import Counter from './components/Counter'
import counter from './reducers'
const store = createStore(counter)
const rootEl = document.getElementById('root')
const render = () => {
return ReactDOM.render(
<Counter
value={store.getState()}
onIncrement={() => store.dispatch({ type: 'INCREMENT' })}
onDecrement={() => store.dispatch({ type: 'DECREMENT' })}
/>,
rootEl
)
}
render()
store.subscribe(render)
使用connect
使用connect可以在组件props处理store相关操作
// index.js
import React from 'react'
import { render } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './components/App'
import rootReducer from './reducers'
const store = createStore(rootReducer)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
// FilterLink.js
import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions'
import Link from '../components/Link'
const mapStateToProps = (state, ownProps) => ({
active: ownProps.filter === state.visibilityFilter
})
const mapDispatchToProps = (dispatch, ownProps) => ({
onClick: () => dispatch(setVisibilityFilter(ownProps.filter))
})
export default connect(mapStateToProps, mapDispatchToProps)(Link)
源码
Provider
解释问题:当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 }) {
const contextValue = useMemo(() => {
const subscription = new Subscription(store)
subscription.onStateChange = subscription.notifyNestedSubs
return {
store,
subscription,
}
}, [store])
const previousState = useMemo(() => store.getState(), [store])
useIsomorphicLayoutEffect(() => {
const { subscription } = contextValue
subscription.trySubscribe()
if (previousState !== store.getState()) {
subscription.notifyNestedSubs()
}
return () => {
subscription.tryUnsubscribe()
subscription.onStateChange = null
}
}, [contextValue, previousState])
const Context = context || ReactReduxContext
return <Context.Provider value={contextValue}>{children}</Context.Provider>
}
……
export default Provider
Provider的作用是处理store相关的listeners,当store发生了变更之后去执行订阅的listener,即触发组件渲染。
1. 初始化
从第一行开始看起
- Subscription类就是用观察者模式去实现依赖收集和事件派发的,观察者模式的代码实现都大同小异。
import { getBatch } from './batch'
// 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
const nullListeners = { notify() {} }
function createListenerCollection() {
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 = {
callback,
next: null,
prev: last,
})
if (listener.prev) {
listener.prev.next = listener
} else {
first = listener
}
return function unsubscribe() {
if (!isSubscribed || first === null) return
isSubscribed = false
if (listener.next) {
listener.next.prev = listener.prev
} else {
last = listener.prev
}
if (listener.prev) {
listener.prev.next = listener.next
} else {
first = listener.next
}
}
},
}
}
export default class Subscription {
constructor(store, parentSub) {
this.store = store
this.parentSub = parentSub
this.unsubscribe = null
this.listeners = nullListeners
this.handleChangeWrapper = this.handleChangeWrapper.bind(this)
}
addNestedSub(listener) {
this.trySubscribe()
return this.listeners.subscribe(listener)
}
notifyNestedSubs() {
this.listeners.notify()
}
handleChangeWrapper() {
if (this.onStateChange) {
this.onStateChange()
}
}
isSubscribed() {
return Boolean(this.unsubscribe)
}
trySubscribe() {
if (!this.unsubscribe) {
this.unsubscribe = this.parentSub
? this.parentSub.addNestedSub(this.handleChangeWrapper)
: this.store.subscribe(this.handleChangeWrapper)
this.listeners = createListenerCollection()
}
}
tryUnsubscribe() {
if (this.unsubscribe) {
this.unsubscribe()
this.unsubscribe = null
this.listeners.clear()
this.listeners = nullListeners
}
}
}
特别的地方在于存储listener的结构是链表,可能是因为频繁插入删除的原因而没有使用数组(记得在vue2的发布订阅模式用的是array)
subscription.onStateChange = subscription.notifyNestedSubs
这行就是onStateChange=this.listeners.notify()
- 当store发生变更时,将会重新计算得到两个memoized 值。一个是contextValue,一个是previousState。
- 使用上文说的两个memoized和useIsomorphicLayoutEffect也就是useLayoutEffect来做数据更新时的重新订阅,即store.subscribe
useIsomorphicLayoutEffect(() => {
const { subscription } = contextValue
// 即this.store.subscribe(this.handleChangeWrapper),将handleChangeWrapper订阅成为store的listener;
//显然,当store发生变化时,redux将会触发这个listener。
subscription.trySubscribe()
if (previousState !== store.getState()) { // 如果store发生变更
subscription.notifyNestedSubs() // 即this.listeners.notify(),触发subscription的listener
}
return () => {
subscription.tryUnsubscribe() // 清除订阅
subscription.onStateChange = null
}
}, [contextValue, previousState])
当store发生变化时会触发this.listeners的所有listener.callback()。
listeners
可以看到listeners链表里面装的callback叫handleChangeWrapper
-
然后利用Context.Provider把得到的contextValue传递下去
contextValue
2. 更新
-
当触发了store的更新时
listener.notify
callback
调用datchedUpdate就可以触发组件的渲染
小结
ProviderProvider做的主要的事情其实就是
this.store.subscribe(this.listeners.notify)
Connect
function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)
It does not modify the component class passed to it; instead, it returns a new, connected component class that wraps the component you passed in.
- connect不会改变传入的Component而是返回包了一层的新组件。
function ConnectFunction(props){
……
const ContextToUse = useMemo(() => {
// 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])
……
const renderedWrappedComponent = useMemo(
() => (
<WrappedComponent
{...actualChildProps}
ref={reactReduxForwardedRef}
/>
),
[reactReduxForwardedRef, 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.
return (
<ContextToUse.Provider value={overriddenContextValue}>
{renderedWrappedComponent}
</ContextToUse.Provider>
)
}
return renderedWrappedComponent
}, [ContextToUse, renderedWrappedComponent, overriddenContextValue])
……
- 新的使用useMemo包过的Component是renderedWrappedComponent,actualChildProps就是传入的处理过的新增props
通过这种方式,在store发生变化时触发了renderedWrappedComponent的重新渲染达到刷新页面的作用
参考
https://segmentfault.com/a/1190000010416732
https://react-redux.js.org/api/connect
https://www.redux.org.cn/docs/react-redux/api.html
https://axiu.me/coding/react-batchedupdates-and-transaction/
网友评论