foreword
The react-redux library provides Provider
components to inject the store into the application through context, and then you can use connect
higher-order methods to obtain and monitor the store, and then calculate the new props according to the store state and the component's own props, inject the component, and listen to the store to compare The calculated new props determine whether the component needs to be updated.
Provider
First, the react-redux library provides Provider
components to inject the store into an entry component of the entire React application, usually the top-level component of the application. Provider
The component uses the context to pass down the store:
// 内部组件获取redux store的键
const storeKey = 'store'
// 内部组件
const subscriptionKey = subKey || `${storeKey}Subscription`
class Provider extends Component {
// 声明context,注入store和可选的发布订阅对象
getChildContext() {
return { [storeKey]: this[storeKey], [subscriptionKey]: null }
}
constructor(props, context) {
super(props, context)
// 缓存store
this[storeKey] = props.store;
}
render() {
// 渲染输出内容
return Children.only(this.props.children)
}
}
Example
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import App from './components/App'
import reducers from './reducers'
// 创建store
const store = createStore(todoApp, reducers)
// 传递store作为props给Provider组件;
// Provider将使用context方式向下传递store
// App组件是我们的应用顶层组件
render(
<Provider store={store}>
<App/>
</Provider>, document.getElementById('app-node')
)
connect method
Earlier we used Provider
components to inject the redux store into the application, the next thing we need to do is to connect the components and the store. And we know that Redux does not provide a way to directly manipulate the store state, we can only getState
access data through it, or dispatch
change the store state through an action.
This is exactly the ability provided by the connect high-order method provided by react-redux.
Example
container/TodoList.js
First, we create a list container component, which is responsible for obtaining the todo list in the component, and then passes the todos to the TodoList display component, and also passes the event callback function. When the display component triggers events such as clicks, the corresponding callback is called. The redux store state is updated through dispatch actions, and the react-redux connect
method is used to finally connect the store and the presentation component , which receives
import {connect} from 'react-redux'
import TodoList from 'components/TodoList.jsx'
class TodoListContainer extends React.Component {
constructor(props) {
super(props)
this.state = {todos: null, filter: null}
}
handleUpdateClick (todo) {
this.props.update(todo);
}
componentDidMount() {
const { todos, filter, actions } = this.props
if (todos.length === 0) {
this.props.fetchTodoList(filter);
}
render () {
const { todos, filter } = this.props
return (
<TodoList
todos={todos}
filter={filter}
handleUpdateClick={this.handleUpdateClick}
/* others */
/>
)
}
}
const mapStateToProps = state => {
return {
todos : state.todos,
filter: state.filter
}
}
const mapDispatchToProps = dispatch => {
return {
update : (todo) => dispatch({
type : 'UPDATE_TODO',
payload: todo
}),
fetchTodoList: (filters) => dispatch({
type : 'FETCH_TODOS',
payload: filters
})
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoListContainer)
components/TodoList.js
import React from 'react'
import PropTypes from 'prop-types'
import Todo from './Todo'
const TodoList = ({ todos, handleUpdateClick }) => (
<ul>
{todos.map(todo => (
<Todo key={todo.id} {...todo} handleUpdateClick={handleUpdateClick} />
))}
</ul>
)
TodoList.propTypes = {
todos: PropTypes.array.isRequired
).isRequired,
handleUpdateClick: PropTypes.func.isRequired
}
export default TodoList
components/Todo.js
import React from 'react'
import PropTypes from 'prop-types'
class Todo extends React.Component {
constructor(...args) {
super(..args);
this.state = {
editable: false,
todo: this.props.todo
}
}
handleClick (e) {
this.setState({
editable: !this.state.editable
})
}
update () {
this.props.handleUpdateClick({
...this.state.todo
text: this.refs.content.innerText
})
}
render () {
return (
<li
onClick={this.handleClick}
style={{
contentEditable: editable ? 'true' : 'false'
}}
>
<p ref="content">{text}</p>
<button onClick={this.update}>Save</button>
</li>
)
}
Todo.propTypes = {
handleUpdateClick: PropTypes.func.isRequired,
text: PropTypes.string.isRequired
}
export default Todo
Container components and presentational components
When using Redux as a state management container for React applications, the practice of dividing components into Container Components and Presentational Components is usually implemented.
Presentational Components | Container Components | |
---|---|---|
Target | UI display (HTML structure and style) | Business logic (get data, update status) |
Aware of Redux | without | have |
Data Sources | props | Subscribe to the Redux store |
change data | Call the callback function passed by props | Dispatch Redux actions |
reusable | Strong independence | High degree of business coupling |
Most of the code in the application is writing presentational components, and then using some container components to connect these presentational components to the Redux store.
connect() source code analysis
connectHOC = connectAdvanced;
mergePropsFactories = defaultMergePropsFactories;
selectorFactory = defaultSelectorFactory;
function connect (
mapStateToProps,
mapDispatchToProps,
mergeProps,
{
pure = true,
areStatesEqual = strictEqual, // 严格比较是否相等
areOwnPropsEqual = shallowEqual, // 浅比较
areStatePropsEqual = shallowEqual,
areMergedPropsEqual = shallowEqual,
renderCountProp, // 传递给内部组件的props键,表示render方法调用次数
// props/context 获取store的键
storeKey = 'store',
...extraOptions
} = {}
) {
const initMapStateToProps = match(mapStateToProps, mapStateToPropsFactories, 'mapStateToProps')
const initMapDispatchToProps = match(mapDispatchToProps, mapDispatchToPropsFactories, 'mapDispatchToProps')
const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')
// 调用connectHOC方法
connectHOC(selectorFactory, {
// 如果mapStateToProps为false,则不监听store state
shouldHandleStateChanges: Boolean(mapStateToProps),
// 传递给selectorFactory
initMapStateToProps,
initMapDispatchToProps,
initMergeProps,
pure,
areStatesEqual,
areOwnPropsEqual,
areStatePropsEqual,
areMergedPropsEqual,
renderCountProp, // 传递给内部组件的props键,表示render方法调用次数
// props/context 获取store的键
storeKey = 'store',
...extraOptions // 其他配置项
});
}
strictEquall
function strictEqual(a, b) { return a === b }
shallowEquall
const hasOwn = Object.prototype.hasOwnProperty
function is(x, y) {
if (x === y) {
return x !== 0 || y !== 0 || 1 / x === 1 / y
} else {
return x !== x && y !== y
}
}
export default function shallowEqual(objA, objB) {
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 (!hasOwn.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])) {
return false
}
}
return true
}
shallowEqual({x:{}},{x:{}}) // false
shallowEqual({x:1},{x:1}) // true
connectAdvanced higher-order function
function connectAdvanced (
selectorFactory,
{
renderCountProp = undefined, // 传递给内部组件的props键,表示render方法调用次数
// props/context 获取store的键
storeKey = 'store',
...connectOptions
} = {}
) {
// 获取发布订阅器的键
const subscriptionKey = storeKey + 'Subscription';
const contextTypes = {
[storeKey]: storeShape,
[subscriptionKey]: subscriptionShape,
};
const childContextTypes = {
[subscriptionKey]: subscriptionShape,
};
return function wrapWithConnect (WrappedComponent) {
const selectorFactoryOptions = {
// 如果mapStateToProps为false,则不监听store state
shouldHandleStateChanges: Boolean(mapStateToProps),
// 传递给selectorFactory
initMapStateToProps,
initMapDispatchToProps,
initMergeProps,
...connectOptions,
...others
renderCountProp, // render调用次数
shouldHandleStateChanges, // 是否监听store state变更
storeKey,
WrappedComponent
}
// 返回拓展过props属性的Connect组件
return hoistStatics(Connect, WrappedComponent)
}
}
selectorFactory
selectorFactory
The function returns a selector function, which calculates new props according to the store state, display component props, and dispatch, and finally injects the container component. The selectorFactory
function structure is as follows:
(dispatch, options) => (state, props) => ({
thing: state.things[props.thingId],
saveThing: fields => dispatch(actionCreators.saveThing(props.thingId, fields)),
})
Note: The state in redux usually refers to the state of the redux store rather than the state of the component, and the props here are the props of the incoming component wrapperComponent.
function defaultSelectorFactory (dispatch, {
initMapStateToProps,
initMapDispatchToProps,
initMergeProps,
...options
}) {
const mapStateToProps = initMapStateToProps(dispatch, options)
const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
const mergeProps = initMergeProps(dispatch, options)
// pure为true表示selectorFactory返回的selector将缓存结果;
// 否则其总是返回一个新对象
const selectorFactory = options.pure
? pureFinalPropsSelectorFactory
: impureFinalPropsSelectorFactory
// 最终执行selector工厂函数返回一个selector
return selectorFactory(
mapStateToProps,
mapDispatchToProps,
mergeProps,
dispatch,
options
);
}
pureFinalPropsSelectorFactory
function pureFinalPropsSelectorFactory (
mapStateToProps,
mapDispatchToProps,
mergeProps,
dispatch,
{ areStatesEqual, areOwnPropsEqual, areStatePropsEqual }
) {
let hasRunAtLeastOnce = false
let state
let ownProps
let stateProps
let dispatchProps
let mergedProps
// 返回合并后的props或state
// handleSubsequentCalls变更后合并;handleFirstCall初次调用
return function pureFinalPropsSelector(nextState, nextOwnProps) {
return hasRunAtLeastOnce
? handleSubsequentCalls(nextState, nextOwnProps)
: handleFirstCall(nextState, nextOwnProps)
}
}
handleFirstCall
function handleFirstCall(firstState, firstOwnProps) {
state = firstState
ownProps = firstOwnProps
stateProps = mapStateToProps(state, ownProps) // store state映射到组件的props
dispatchProps = mapDispatchToProps(dispatch, ownProps)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps) // 合并后的props
hasRunAtLeastOnce = true
return mergedProps
}
defaultMergeProps
export function defaultMergeProps(stateProps, dispatchProps, ownProps) {
// 默认合并props函数
return { ...ownProps, ...stateProps, ...dispatchProps }
}
handleSubsequentCalls
function handleSubsequentCalls(nextState, nextOwnProps) {
// shallowEqual浅比较
const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps)
// 深比较
const stateChanged = !areStatesEqual(nextState, state)
state = nextState
ownProps = nextOwnProps
// 处理props或state变更后的合并
// store state及组件props变更
if (propsChanged && stateChanged) return handleNewPropsAndNewState()
if (propsChanged) return handleNewProps()
if (stateChanged) return handleNewState()
return mergedProps
}
Compute returns new props
As long as the display component's own props change, you need to go back to the newly merged props, and then update the container component, regardless of whether the store state changes:
// 只有展示型组件props变更
function handleNewProps() {
// mapStateToProps计算是否依赖于展示型组件props
if (mapStateToProps.dependsOnOwnProps)
stateProps = mapStateToProps(state, ownProps)
// mapDispatchToProps计算是否依赖于展示型组件props
if (mapDispatchToProps.dependsOnOwnProps)
dispatchProps = mapDispatchToProps(dispatch, ownProps)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
return mergedProps
}
// 展示型组件props和store state均变更
function handleNewPropsAndNewState() {
stateProps = mapStateToProps(state, ownProps)
// mapDispatchToProps计算是否依赖于展示型组件props
if (mapDispatchToProps.dependsOnOwnProps)
dispatchProps = mapDispatchToProps(dispatch, ownProps)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
return mergedProps
}
Compute returns stateProps
Usually container component props changes are driven by store state changes, so only store state changes are common, and this is where you need to pay attention when using Immutable: do not mapStateToProps
use methods within toJS()
methods.
When mapStateToProps
the props object returned twice has not changed, there is no need to recalculate, just return the props object obtained by the merge before, and then compare the return value of the selector function twice in the selector tracking object to see if there is any change, it will return false, Container components do not trigger changes.
Because shallow comparison is used when comparing the results returned by mapStateToProps multiple times, the Immutable.toJS() method is not recommended. It returns a new object each time, and the comparison will return false. If Immutable is used and its content has not changed, then will return true, which can reduce unnecessary re-rendering.
// 只有store state变更
function handleNewState() {
const nextStateProps = mapStateToProps(state, ownProps)
// 浅比较
const statePropsChanged = !areStatePropsEqual(nextStateProps, stateProps)
stateProps = nextStateProps
// 计算得到的新props变更了,才需要重新计算返回新的合并props
if (statePropsChanged) {
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
}
// 若新stateProps未发生变更,则直接返回上一次计算得出的合并props;
// 之后selector追踪对象比较两次返回值是否有变更时将返回false;
// 否则返回使用mergeProps()方法新合并得到的props对象,变更比较将返回true
return mergedProps
}
hoist-non-react-statics
Similar to Object.assign, the non-React static properties or methods of the child component are copied to the parent component, and the React-related properties or methods will not be overridden but merged.
hoistStatics(Connect, WrappedComponent)
Connect Component
The real Connect high-order component connects the redux store state and incoming components, that is, maps the store state to the component props, react-redux uses the Provider component to inject the store through the context, and then the Connect component receives the store through the context and adds a subscription to the store :
class Connect extends Component {
constructor(props, context) {
super(props, context)
this.state = {}
this.renderCount = 0 // render调用次数初始为0
// 获取store,props或context方式
this.store = props[storeKey] || context[storeKey]
// 是否使用props方式传递store
this.propsMode = Boolean(props[storeKey])
// 初始化selector
this.initSelector()
// 初始化store订阅
this.initSubscription()
}
componentDidMount() {
// 不需要监听state变更
if (!shouldHandleStateChanges) return
// 发布订阅器执行订阅
this.subscription.trySubscribe()
// 执行selector
this.selector.run(this.props)
// 若还需要更新,则强制更新
if (this.selector.shouldComponentUpdate) this.forceUpdate()
}
// 渲染组件元素
render() {
const selector = this.selector
selector.shouldComponentUpdate = false; // 重置是否需要更新为默认的false
// 将redux store state转化映射得到的props合并入传入的组件
return createElement(WrappedComponent, this.addExtraProps(selector.props))
}
}
addExtraProps()
Add additional props properties to props:
// 添加额外的props
addExtraProps(props) {
const withExtras = { ...props }
if (renderCountProp) withExtras[renderCountProp] = this.renderCount++;// render 调用次数
if (this.propsMode && this.subscription) withExtras[subscriptionKey] = this.subscription
return withExtras
}
Initialize selector tracking object initSelector
Selector, the selector, according to the redux store state and the component's own props, calculates the new props that will be injected into the component, caches the new props, and then compares the props obtained when the selector is executed again to determine whether the component needs to be updated. If the props change, the component is updated, otherwise it is not updated.
Use the initSelector
method to initialize the selector tracking object and related state and data:
// 初始化selector
initSelector() {
// 使用selector工厂函数创建一个selector
const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions)
// 连接组件的selector和redux store state
this.selector = makeSelectorStateful(sourceSelector, this.store)
// 执行组件的selector函数
this.selector.run(this.props)
}
makeSelectorStateful()
Create a selector tracking object to track the results returned by the selector function:
function makeSelectorStateful(sourceSelector, store) {
// 返回selector追踪对象,追踪传入的selector(sourceSelector)返回的结果
const selector = {
// 执行组件的selector函数
run: function runComponentSelector(props) {
// 根据store state和组件props执行传入的selector函数,计算得到nextProps
const nextProps = sourceSelector(store.getState(), props)
// 比较nextProps和缓存的props;
// false,则更新所缓存的props并标记selector需要更新
if (nextProps !== selector.props || selector.error) {
selector.shouldComponentUpdate = true // 标记需要更新
selector.props = nextProps // 缓存props
selector.error = null
}
}
}
// 返回selector追踪对象
return selector
}
Initialize subscription initSubscription
Initialize listening/subscribing to redux store state:
// 初始化订阅
initSubscription() {
if (!shouldHandleStateChanges) return; // 不需要监听store state
// 判断订阅内容传递方式:props或context,两者不能混杂
const parentSub = (this.propsMode ? this.props : this.context)[subscriptionKey]
// 订阅对象实例化,并传入事件回调函数
this.subscription = new Subscription(this.store,
parentSub,
this.onStateChange.bind(this))
// 缓存订阅器发布方法执行的作用域
this.notifyNestedSubs = this.subscription.notifyNestedSubs
.bind(this.subscription)
}
Subscription class implementation
The subscription publisher implementation used by the component subscription store:
export default class Subscription {
constructor(store, parentSub, onStateChange) {
// redux store
this.store = store
// 订阅内容
this.parentSub = parentSub
// 订阅内容变更后的回调函数
this.onStateChange = onStateChange
this.unsubscribe = null
// 订阅记录数组
this.listeners = nullListeners
}
// 订阅
trySubscribe() {
if (!this.unsubscribe) {
// 若传递了发布订阅器则使用该订阅器订阅方法进行订阅
// 否则使用store的订阅方法
this.unsubscribe = this.parentSub
? this.parentSub.addNestedSub(this.onStateChange)
: this.store.subscribe(this.onStateChange)
// 创建订阅集合对象
// { notify: function, subscribe: function }
// 内部包装了一个发布订阅器;
// 分别对应发布(执行所有回调),订阅(在订阅集合中添加回调)
this.listeners = createListenerCollection()
}
}
// 发布
notifyNestedSubs() {
this.listeners.notify()
}
}
Subscription callback function
Callback function executed after subscription:
onStateChange() {
// 选择器执行
this.selector.run(this.props)
if (!this.selector.shouldComponentUpdate) {
// 不需要更新则直接发布
this.notifyNestedSubs()
} else {
// 需要更新则设置组件componentDidUpdate生命周期方法
this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate
// 同时调用setState触发组件更新
this.setState(dummyState) // dummyState = {}
}
}
// 在组件componentDidUpdate生命周期方法内发布变更
notifyNestedSubsOnComponentDidUpdate() {
// 清除组件componentDidUpdate生命周期方法
this.componentDidUpdate = undefined
// 发布
this.notifyNestedSubs()
}
Other lifecycle methods
getChildContext () {
// 若存在props传递了store,则需要对其他从context接收store并订阅的后代组件隐藏其对于store的订阅;
// 否则将父级的订阅器映射传入,给予Connect组件控制发布变化的顺序流
const subscription = this.propsMode ? null : this.subscription
return { [subscriptionKey]: subscription || this.context[subscriptionKey] }
}
// 接收到新props
componentWillReceiveProps(nextProps) {
this.selector.run(nextProps)
}
// 是否需要更新组件
shouldComponentUpdate() {
return this.selector.shouldComponentUpdate
}
componentWillUnmount() {
// 重置selector
}