標籤:

React-Redux源碼分析

Redux,作為大型React應用狀態管理最常用的工具,其概念理論和實踐都是很值得我們學習,分析然後在實踐中深入了解的,對前端開發者能力成長很有幫助。本篇計劃結合Redux容器組件和展示型組件的區別對比以及Redux與React應用最常見的連接庫,react-redux源碼分析,以期達到對Redux和React應用的更深層次理解。

歡迎訪問我的個人博客

前言

react-redux庫提供Provider組件通過context方式嚮應用注入store,然後可以使用connect高階方法,獲取並監聽store,然後根據store state和組件自身props計算得到新props,注入該組件,並且可以通過監聽store,比較計算出的新props判斷是否需要更新組件。

Provider

首先,react-redux庫提供Provider組件將store注入整個React應用的某個入口組件,通常是應用的頂層組件。Provider組件使用context向下傳遞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-reduximport { createStore } from reduximport App from ./components/Appimport reducers from ./reducers// 創建storeconst store = createStore(todoApp, reducers)// 傳遞store作為props給Provider組件;// Provider將使用context方式向下傳遞store// App組件是我們的應用頂層組件render( <Provider store={store}> <App/> </Provider>, document.getElementById(app-node))

connect方法

在前面我們使用Provider組件將redux store注入應用,接下來需要做的是連接組件和store。而且我們知道Redux不提供直接操作store state的方式,我們只能通過其getState訪問數據,或通過dispatch一個action來改變store state。

這也正是react-redux提供的connect高階方法所提供的能力。

Example

CONTAINER/TODOLIST.JS

首先我們創建一個列表容器組件,在組件內負責獲取todo列表,然後將todos傳遞給TodoList展示型組件,同時傳遞事件回調函數,展示型組件觸發諸如點擊等事件時,調用對應回調,這些回調函數內通過dispatch actions來更新redux store state,而最終將store和展示型組件連接起來使用的是react-redux的connect方法,該方法接收

import {connect} from react-reduximport TodoList from components/TodoList.jsxclass 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 reactimport PropTypes from prop-typesimport Todo from ./Todoconst 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 reactimport PropTypes from prop-typesclass 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} stylex={{ 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 ComponentsContainer Components目標UI展示 (HTML結構和樣式)業務邏輯(獲取數據,更新狀態)感知Redux無有數據來源props訂閱Redux store變更數據調用props傳遞的回調函數Dispatch Redux actions可重用獨立性強業務耦合度高

應用中大部分代碼是在編寫展示型組件,然後使用一些容器組件將這些展示型組件和Redux store連接起來。

connect()源碼分析

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.hasOwnPropertyfunction 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:{}}) // falseshallowEqual({x:1},{x:1}) // true

connectAdvanced高階函數

源碼

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函數返回一個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 );}

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}

計算返回新PROPS

只要展示型組件自身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}

計算返回STATEPROPS

通常容器組件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}

hoist-non-react-statics

類似Object.assign,將子組件的非React的靜態屬性或方法複製到父組件,React相關屬性或方法不會被覆蓋而是合併。

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調用次數初始為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()

給props添加額外的props屬性:

// 添加額外的propsaddExtraProps(props) { const withExtras = { ...props } if (renderCountProp) withExtras[renderCountProp] = this.renderCount++;// render 調用次數 if (this.propsMode && this.subscription) withExtras[subscriptionKey] = this.subscription return withExtras}

初始化selector追蹤對象initSelector

Selector,選擇器,根據redux store state和組件的自身props,計算出將注入該組件的新props,並緩存新props,之後再次執行選擇器時通過對比得出的props,決定是否需要更新組件,若props變更則更新組件,否則不更新。

使用initSelector方法初始化selector追蹤對象及相關狀態和數據:

// 初始化selectorinitSelector() { // 使用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()

創建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}

初始化訂閱initSubscription

初始化監聽/訂閱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] }}// 接收到新propscomponentWillReceiveProps(nextProps) { this.selector.run(nextProps)}// 是否需要更新組件shouldComponentUpdate() { return this.selector.shouldComponentUpdate}componentWillUnmount() { // 重置selector}

參考閱讀

  1. React with redux
  2. Smart and Dumb Components
  3. React Redux Container Pattern

推薦閱讀:

美化上傳文件組件
現在(2017年)是否還有必要學習jQuery?
React 16 中的異常處理
Weex 和 React Native 的根本區別在哪裡?

TAG:React | Redux |