Redux,做为大型React应用状态管理最经常使用的工具,其概念理论和实践都是很值得咱们学习,分析而后在实践中深刻了解的,对前端开发者能力成长颇有帮助。本篇计划结合Redux容器组件和展现型组件的区别对比以及Redux与React应用最多见的链接库,react-redux源码分析,以期达到对Redux和React应用的更深层次理解。javascript
欢迎访问个人我的博客html
react-redux库提供Provider
组件经过context方式向应用注入store,而后可使用connect
高阶方法,获取并监听store,而后根据store state和组件自身props计算获得新props,注入该组件,而且能够经过监听store,比较计算出的新props判断是否须要更新组件。前端
首先,react-redux库提供Provider
组件将store注入整个React应用的某个入口组件,一般是应用的顶层组件。Provider
组件使用context向下传递store:java
// 内部组件获取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)
}
}复制代码
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')
)复制代码
在前面咱们使用Provider
组件将redux store注入应用,接下来须要作的是链接组件和store。并且咱们知道Redux不提供直接操做store state的方式,咱们只能经过其getState
访问数据,或经过dispatch
一个action来改变store state。node
这也正是react-redux提供的connect高阶方法所提供的能力。react
container/TodoList.js
首先咱们建立一个列表容器组件,在组件内负责获取todo列表,而后将todos传递给TodoList展现型组件,同时传递事件回调函数,展现型组件触发诸如点击等事件时,调用对应回调,这些回调函数内经过dispatch actions来更新redux store state,而最终将store和展现型组件链接起来使用的是react-redux的connect
方法,该方法接收git
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复制代码
在使用Redux做为React应用的状态管理容器时,一般贯彻将组件划分为容器组件(Container Components)和展现型组件(Presentational Components)的作法,github
Presentational Components | Container Components | |
---|---|---|
目标 | UI展现 (HTML结构和样式) | 业务逻辑(获取数据,更新状态) |
感知Redux | 无 | 有 |
数据来源 | props | 订阅Redux store |
变动数据 | 调用props传递的回调函数 | Dispatch Redux actions |
可重用 | 独立性强 | 业务耦合度高 |
应用中大部分代码是在编写展现型组件,而后使用一些容器组件将这些展现型组件和Redux store链接起来。redux
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 // 其余配置项
});
}复制代码
function strictEqual(a, b) { return a === b }复制代码
源码数组
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复制代码
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
函数返回一个selector函数,根据store state, 展现型组件props,和dispatch计算获得新props,最后注入容器组件,selectorFactory
函数结构形如:
(dispatch, options) => (state, props) => ({
thing: state.things[props.thingId],
saveThing: fields => dispatch(actionCreators.saveThing(props.thingId, fields)),
})复制代码
注:redux中的state一般指redux store的state而不是组件的state,另此处的props为传入组件wrapperComponent的props。
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
);
}复制代码
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)
}
}复制代码
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
}复制代码
export function defaultMergeProps(stateProps, dispatchProps, ownProps) {
// 默认合并props函数
return { ...ownProps, ...stateProps, ...dispatchProps }
}复制代码
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
}复制代码
只要展现型组件自身props发生变动,则须要从新返回新合并props,而后更新容器组件,不管store state是否变动:
// 只有展现型组件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
}复制代码
一般容器组件props变动由store state变动推进,因此只有store state变动的状况较多,并且此处也正是使用Immutable时须要注意的地方:不要在mapStateToProps
方法内使用toJS()
方法。
当mapStateToProps
两次返回的props对象未有变动时,不须要从新计算,直接返回以前合并获得的props对象便可,以后在selector追踪对象中比较两次selector函数返回值是否有变动时,将返回false,容器组件不会触发变动。
由于对比屡次mapStateToProps返回的结果时是使用浅比较,因此不推荐使用Immutable.toJS()方法,其每次均返回一个新对象,对比将返回false,而若是使用Immutable且其内容未变动,则会返回true,能够减小没必要要的从新渲染。
// 只有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
}复制代码
相似Object.assign,将子组件的非React的静态属性或方法复制到父组件,React相关属性或方法不会被覆盖而是合并。
hoistStatics(Connect, WrappedComponent)复制代码
真正的Connect高阶组件,链接redux store state和传入组件,即将store state映射到组件props,react-redux使用Provider组件经过context方式注入store,而后Connect组件经过context接收store,并添加对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))
}
}复制代码
给props添加额外的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
}复制代码
Selector,选择器,根据redux store state和组件的自身props,计算出将注入该组件的新props,并缓存新props,以后再次执行选择器时经过对比得出的props,决定是否须要更新组件,若props变动则更新组件,不然不更新。
使用initSelector
方法初始化selector追踪对象及相关状态和数据:
// 初始化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)
}复制代码
建立selector追踪对象以追踪(tracking)selector函数返回结果:
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
}复制代码
初始化监听/订阅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)
}复制代码
组件订阅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()
}
}复制代码
订阅后执行的回调函数:
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()
}复制代码
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
}复制代码