Redux,做爲大型React應用狀態管理最經常使用的工具,其概念理論和實踐都是很值得咱們學習,分析而後在實踐中深刻了解的,對前端開發者能力成長頗有幫助。本篇計劃結合Redux容器組件和展現型組件的區別對比以及Redux與React應用最多見的鏈接庫,react-redux源碼分析,以期達到對Redux和React應用的更深層次理解。html
react-redux庫提供Provider
組件經過context方式嚮應用注入store,而後可使用connect
高階方法,獲取並監聽store,而後根據store state和組件自身props計算獲得新props,注入該組件,而且能夠經過監聽store,比較計算出的新props判斷是否須要更新組件。node
首先,react-redux庫提供Provider
組件將store注入整個React應用的某個入口組件,一般是應用的頂層組件。Provider
組件使用context向下傳遞store:react
// 內部組件獲取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。git
這也正是react-redux提供的connect高階方法所提供的能力。github
container/TodoList.js
首先咱們建立一個列表容器組件,在組件內負責獲取todo列表,而後將todos傳遞給TodoList展現型組件,同時傳遞事件回調函數,展現型組件觸發諸如點擊等事件時,調用對應回調,這些回調函數內經過dispatch actions來更新redux store state,而最終將store和展現型組件鏈接起來使用的是react-redux的connect
方法,該方法接收redux
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)的作法,數組
Presentational Components | Container Components | |
---|---|---|
目標 | UI展現 (HTML結構和樣式) | 業務邏輯(獲取數據,更新狀態) |
感知Redux | 無 | 有 |
數據來源 | props | 訂閱Redux store |
變動數據 | 調用props傳遞的回調函數 | Dispatch Redux actions |
可重用 | 獨立性強 | 業務耦合度高 |
應用中大部分代碼是在編寫展現型組件,而後使用一些容器組件將這些展現型組件和Redux store鏈接起來。緩存
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 }複製代碼
源碼markdown
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
}複製代碼