React-Redux分析
Redux弱贼,作為大型React應(yīng)用狀態(tài)管理最常用的工具,其概念理論和實(shí)踐都是很值得我們學(xué)習(xí)呕诉,分析然后在實(shí)踐中深入了解的猖毫,對前端開發(fā)者能力成長很有幫助。本篇計(jì)劃結(jié)合Redux容器組件和展示型組件的區(qū)別對比以及Redux與React應(yīng)用最常見的連接庫镐依,react-redux源碼分析匹涮,以期達(dá)到對Redux和React應(yīng)用的更深層次理解。
前言
react-redux庫提供Provider
組件通過context方式向應(yīng)用注入store槐壳,然后可以使用connect
高階方法然低,獲取并監(jiān)聽store,然后根據(jù)store state和組件自身props計(jì)算得到新props,注入該組件雳攘,并且可以通過監(jiān)聽store带兜,比較計(jì)算出的新props判斷是否需要更新組件。
Provider
首先吨灭,react-redux庫提供Provider
組件將store注入整個(gè)React應(yīng)用的某個(gè)入口組件刚照,通常是應(yīng)用的頂層組件。Provider
組件使用context向下傳遞store:
// 內(nèi)部組件獲取redux store的鍵
const storeKey = 'store'
// 內(nèi)部組件
const subscriptionKey = subKey || `${storeKey}Subscription`
class Provider extends Component {
// 聲明context喧兄,注入store和可選的發(fā)布訂閱對象
getChildContext() {
return { [storeKey]: this[storeKey], [subscriptionKey]: null }
}
constructor(props, context) {
super(props, context)
// 緩存store
this[storeKey] = props.store;
}
render() {
// 渲染輸出內(nèi)容
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'
// 創(chuàng)建store
const store = createStore(todoApp, reducers)
// 傳遞store作為props給Provider組件无畔;
// Provider將使用context方式向下傳遞store
// App組件是我們的應(yīng)用頂層組件
render(
<Provider store={store}>
<App/>
</Provider>, document.getElementById('app-node')
)
connect方法
在前面我們使用Provider
組件將redux store注入應(yīng)用,接下來需要做的是連接組件和store吠冤。而且我們知道Redux不提供直接操作store state的方式浑彰,我們只能通過其getState
訪問數(shù)據(jù),或通過dispatch
一個(gè)action來改變store state拯辙。
這也正是react-redux提供的connect高階方法所提供的能力郭变。
Example
container/TodoList.js
首先我們創(chuàng)建一個(gè)列表容器組件,在組件內(nèi)負(fù)責(zé)獲取todo列表薄风,然后將todos傳遞給TodoList展示型組件饵较,同時(shí)傳遞事件回調(diào)函數(shù),展示型組件觸發(fā)諸如點(diǎn)擊等事件時(shí)遭赂,調(diào)用對應(yīng)回調(diào)循诉,這些回調(diào)函數(shù)內(nèi)通過dispatch actions來更新redux store state,而最終將store和展示型組件連接起來使用的是react-redux的connect
方法撇他,該方法接收
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應(yīng)用的狀態(tài)管理容器時(shí)茄猫,通常貫徹將組件劃分為容器組件(Container Components)和展示型組件(Presentational Components)的做法,
Presentational Components | Container Components | |
---|---|---|
目標(biāo) | UI展示 (HTML結(jié)構(gòu)和樣式) | 業(yè)務(wù)邏輯(獲取數(shù)據(jù)困肩,更新狀態(tài)) |
感知Redux | 無 | 有 |
數(shù)據(jù)來源 | props | 訂閱Redux store |
變更數(shù)據(jù) | 調(diào)用props傳遞的回調(diào)函數(shù) | Dispatch Redux actions |
可重用 | 獨(dú)立性強(qiáng) | 業(yè)務(wù)耦合度高 |
應(yīng)用中大部分代碼是在編寫展示型組件划纽,然后使用一些容器組件將這些展示型組件和Redux store連接起來。
connect()源碼分析
connectHOC = connectAdvanced;
mergePropsFactories = defaultMergePropsFactories锌畸;
selectorFactory = defaultSelectorFactory;
function connect (
mapStateToProps,
mapDispatchToProps,
mergeProps,
{
pure = true,
areStatesEqual = strictEqual, // 嚴(yán)格比較是否相等
areOwnPropsEqual = shallowEqual, // 淺比較
areStatePropsEqual = shallowEqual,
areMergedPropsEqual = shallowEqual,
renderCountProp, // 傳遞給內(nèi)部組件的props鍵勇劣,表示render方法調(diào)用次數(shù)
// props/context 獲取store的鍵
storeKey = 'store',
...extraOptions
} = {}
) {
const initMapStateToProps = match(mapStateToProps, mapStateToPropsFactories, 'mapStateToProps')
const initMapDispatchToProps = match(mapDispatchToProps, mapDispatchToPropsFactories, 'mapDispatchToProps')
const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')
// 調(diào)用connectHOC方法
connectHOC(selectorFactory, {
// 如果mapStateToProps為false,則不監(jiān)聽store state
shouldHandleStateChanges: Boolean(mapStateToProps),
// 傳遞給selectorFactory
initMapStateToProps,
initMapDispatchToProps,
initMergeProps,
pure,
areStatesEqual,
areOwnPropsEqual,
areStatePropsEqual,
areMergedPropsEqual,
renderCountProp, // 傳遞給內(nèi)部組件的props鍵潭枣,表示render方法調(diào)用次數(shù)
// props/context 獲取store的鍵
storeKey = 'store',
...extraOptions // 其他配置項(xiàng)
});
}
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高階函數(shù)
function connectAdvanced (
selectorFactory,
{
renderCountProp = undefined, // 傳遞給內(nèi)部組件的props鍵比默,表示render方法調(diào)用次數(shù)
// props/context 獲取store的鍵
storeKey = 'store',
...connectOptions
} = {}
) {
// 獲取發(fā)布訂閱器的鍵
const subscriptionKey = storeKey + 'Subscription';
const contextTypes = {
[storeKey]: storeShape,
[subscriptionKey]: subscriptionShape,
};
const childContextTypes = {
[subscriptionKey]: subscriptionShape,
};
return function wrapWithConnect (WrappedComponent) {
const selectorFactoryOptions = {
// 如果mapStateToProps為false,則不監(jiān)聽store state
shouldHandleStateChanges: Boolean(mapStateToProps),
// 傳遞給selectorFactory
initMapStateToProps,
initMapDispatchToProps,
initMergeProps,
...connectOptions,
...others
renderCountProp, // render調(diào)用次數(shù)
shouldHandleStateChanges, // 是否監(jiān)聽store state變更
storeKey,
WrappedComponent
}
// 返回拓展過props屬性的Connect組件
return hoistStatics(Connect, WrappedComponent)
}
}
selectorFactory
selectorFactory
函數(shù)返回一個(gè)selector函數(shù)盆犁,根據(jù)store state, 展示型組件props,和dispatch計(jì)算得到新props命咐,最后注入容器組件,selectorFactory
函數(shù)結(jié)構(gòu)形如:
(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將緩存結(jié)果榛臼;
// 否則其總是返回一個(gè)新對象
const selectorFactory = options.pure
? pureFinalPropsSelectorFactory
: impureFinalPropsSelectorFactory
// 最終執(zhí)行selector工廠函數(shù)返回一個(gè)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初次調(diào)用
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) {
// 默認(rèn)合并props函數(shù)
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
}
計(jì)算返回新props
只要展示型組件自身props發(fā)生變更窜司,則需要重新返回新合并props沛善,然后更新容器組件,無論store state是否變更:
// 只有展示型組件props變更
function handleNewProps() {
// mapStateToProps計(jì)算是否依賴于展示型組件props
if (mapStateToProps.dependsOnOwnProps)
stateProps = mapStateToProps(state, ownProps)
// mapDispatchToProps計(jì)算是否依賴于展示型組件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計(jì)算是否依賴于展示型組件props
if (mapDispatchToProps.dependsOnOwnProps)
dispatchProps = mapDispatchToProps(dispatch, ownProps)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
return mergedProps
}
計(jì)算返回stateProps
通常容器組件props變更由store state變更推動(dòng)例证,所以只有store state變更的情況較多路呜,而且此處也正是使用Immutable時(shí)需要注意的地方:不要在mapStateToProps
方法內(nèi)使用toJS()
方法迷捧。
當(dāng)mapStateToProps
兩次返回的props對象未有變更時(shí)织咧,不需要重新計(jì)算,直接返回之前合并得到的props對象即可漠秋,之后在selector追蹤對象中比較兩次selector函數(shù)返回值是否有變更時(shí)笙蒙,將返回false,容器組件不會(huì)觸發(fā)變更庆锦。
因?yàn)閷Ρ榷啻蝝apStateToProps返回的結(jié)果時(shí)是使用淺比較捅位,所以不推薦使用Immutable.toJS()方法,其每次均返回一個(gè)新對象搂抒,對比將返回false艇搀,而如果使用Immutable且其內(nèi)容未變更,則會(huì)返回true求晶,可以減少不必要的重新渲染焰雕。
// 只有store state變更
function handleNewState() {
const nextStateProps = mapStateToProps(state, ownProps)
// 淺比較
const statePropsChanged = !areStatePropsEqual(nextStateProps, stateProps)
stateProps = nextStateProps
// 計(jì)算得到的新props變更了,才需要重新計(jì)算返回新的合并props
if (statePropsChanged) {
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
}
// 若新stateProps未發(fā)生變更芳杏,則直接返回上一次計(jì)算得出的合并props矩屁;
// 之后selector追蹤對象比較兩次返回值是否有變更時(shí)將返回false;
// 否則返回使用mergeProps()方法新合并得到的props對象爵赵,變更比較將返回true
return mergedProps
}
hoist-non-react-statics
類似Object.assign吝秕,將子組件的非React的靜態(tài)屬性或方法復(fù)制到父組件,React相關(guān)屬性或方法不會(huì)被覆蓋而是合并空幻。
hoistStatics(Connect, WrappedComponent)
Connect Component
真正的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調(diào)用次數(shù)初始為0
// 獲取store如捅,props或context方式
this.store = props[storeKey] || context[storeKey]
// 是否使用props方式傳遞store
this.propsMode = Boolean(props[storeKey])
// 初始化selector
this.initSelector()
// 初始化store訂閱
this.initSubscription()
}
componentDidMount() {
// 不需要監(jiān)聽state變更
if (!shouldHandleStateChanges) return
// 發(fā)布訂閱器執(zhí)行訂閱
this.subscription.trySubscribe()
// 執(zhí)行selector
this.selector.run(this.props)
// 若還需要更新棍现,則強(qiáng)制更新
if (this.selector.shouldComponentUpdate) this.forceUpdate()
}
// 渲染組件元素
render() {
const selector = this.selector
selector.shouldComponentUpdate = false; // 重置是否需要更新為默認(rèn)的false
// 將redux store state轉(zhuǎn)化映射得到的props合并入傳入的組件
return createElement(WrappedComponent, this.addExtraProps(selector.props))
}
}
addExtraProps()
給props添加額外的props屬性:
// 添加額外的props
addExtraProps(props) {
const withExtras = { ...props }
if (renderCountProp) withExtras[renderCountProp] = this.renderCount++;// render 調(diào)用次數(shù)
if (this.propsMode && this.subscription) withExtras[subscriptionKey] = this.subscription
return withExtras
}
初始化selector追蹤對象initSelector
Selector,選擇器镜遣,根據(jù)redux store state和組件的自身props己肮,計(jì)算出將注入該組件的新props士袄,并緩存新props,之后再次執(zhí)行選擇器時(shí)通過對比得出的props谎僻,決定是否需要更新組件娄柳,若props變更則更新組件,否則不更新艘绍。
使用initSelector
方法初始化selector追蹤對象及相關(guān)狀態(tài)和數(shù)據(jù):
// 初始化selector
initSelector() {
// 使用selector工廠函數(shù)創(chuàng)建一個(gè)selector
const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions)
// 連接組件的selector和redux store state
this.selector = makeSelectorStateful(sourceSelector, this.store)
// 執(zhí)行組件的selector函數(shù)
this.selector.run(this.props)
}
makeSelectorStateful()
創(chuàng)建selector追蹤對象以追蹤(tracking)selector函數(shù)返回結(jié)果:
function makeSelectorStateful(sourceSelector, store) {
// 返回selector追蹤對象赤拒,追蹤傳入的selector(sourceSelector)返回的結(jié)果
const selector = {
// 執(zhí)行組件的selector函數(shù)
run: function runComponentSelector(props) {
// 根據(jù)store state和組件props執(zhí)行傳入的selector函數(shù),計(jì)算得到nextProps
const nextProps = sourceSelector(store.getState(), props)
// 比較nextProps和緩存的props;
// false诱鞠,則更新所緩存的props并標(biāo)記selector需要更新
if (nextProps !== selector.props || selector.error) {
selector.shouldComponentUpdate = true // 標(biāo)記需要更新
selector.props = nextProps // 緩存props
selector.error = null
}
}
}
// 返回selector追蹤對象
return selector
}
初始化訂閱initSubscription
初始化監(jiān)聽/訂閱redux store state:
// 初始化訂閱
initSubscription() {
if (!shouldHandleStateChanges) return; // 不需要監(jiān)聽store state
// 判斷訂閱內(nèi)容傳遞方式:props或context挎挖,兩者不能混雜
const parentSub = (this.propsMode ? this.props : this.context)[subscriptionKey]
// 訂閱對象實(shí)例化,并傳入事件回調(diào)函數(shù)
this.subscription = new Subscription(this.store,
parentSub,
this.onStateChange.bind(this))
// 緩存訂閱器發(fā)布方法執(zhí)行的作用域
this.notifyNestedSubs = this.subscription.notifyNestedSubs
.bind(this.subscription)
}
訂閱類實(shí)現(xiàn)
組件訂閱store使用的訂閱發(fā)布器實(shí)現(xiàn):
export default class Subscription {
constructor(store, parentSub, onStateChange) {
// redux store
this.store = store
// 訂閱內(nèi)容
this.parentSub = parentSub
// 訂閱內(nèi)容變更后的回調(diào)函數(shù)
this.onStateChange = onStateChange
this.unsubscribe = null
// 訂閱記錄數(shù)組
this.listeners = nullListeners
}
// 訂閱
trySubscribe() {
if (!this.unsubscribe) {
// 若傳遞了發(fā)布訂閱器則使用該訂閱器訂閱方法進(jìn)行訂閱
// 否則使用store的訂閱方法
this.unsubscribe = this.parentSub
? this.parentSub.addNestedSub(this.onStateChange)
: this.store.subscribe(this.onStateChange)
// 創(chuàng)建訂閱集合對象
// { notify: function, subscribe: function }
// 內(nèi)部包裝了一個(gè)發(fā)布訂閱器航夺;
// 分別對應(yīng)發(fā)布(執(zhí)行所有回調(diào))蕉朵,訂閱(在訂閱集合中添加回調(diào))
this.listeners = createListenerCollection()
}
}
// 發(fā)布
notifyNestedSubs() {
this.listeners.notify()
}
}
訂閱回調(diào)函數(shù)
訂閱后執(zhí)行的回調(diào)函數(shù):
onStateChange() {
// 選擇器執(zhí)行
this.selector.run(this.props)
if (!this.selector.shouldComponentUpdate) {
// 不需要更新則直接發(fā)布
this.notifyNestedSubs()
} else {
// 需要更新則設(shè)置組件componentDidUpdate生命周期方法
this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate
// 同時(shí)調(diào)用setState觸發(fā)組件更新
this.setState(dummyState) // dummyState = {}
}
}
// 在組件componentDidUpdate生命周期方法內(nèi)發(fā)布變更
notifyNestedSubsOnComponentDidUpdate() {
// 清除組件componentDidUpdate生命周期方法
this.componentDidUpdate = undefined
// 發(fā)布
this.notifyNestedSubs()
}
其他生命周期方法
getChildContext () {
// 若存在props傳遞了store,則需要對其他從context接收store并訂閱的后代組件隱藏其對于store的訂閱阳掐;
// 否則將父級的訂閱器映射傳入始衅,給予Connect組件控制發(fā)布變化的順序流
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
}