React-Redux分析
zhjj511
7年前
<p>Redux,作为大型React应用状态管理最常用的工具,其概念理论和实践都是很值得我们学习,分析然后在实践中深入了解的,对前端开发者能力成长很有帮助。本篇计划结合Redux容器组件和展示型组件的区别对比以及Redux与React应用最常见的连接库,react-redux源码分析,以期达到对Redux和React应用的更深层次理解。</p> <p>索引</p> <ul> <li> <ul> <li> <ul> <li>3.1.1 container/TodoList.js</li> <li>3.1.2 components/TodoList.js</li> <li>3.1.3 components/Todo.js</li> </ul> </li> </ul> </li> <li>4 容器组件与展示型组件</li> <li>6 connectAdvanced高阶函数 <ul> <li> <ul> <li>6.1.1 pureFinalPropsSelectorFactory</li> <li>6.1.2 handleFirstCall</li> <li>6.1.3 defaultMergeProps</li> <li>6.1.4 handleSubsequentCalls</li> <li>6.1.5 计算返回新props</li> <li>6.1.6 计算返回stateProps</li> </ul> </li> </ul> </li> <li>7 hoist-non-react-statics</li> </ul> <h2>前言</h2> <p>react-redux库提供 Provider 组件通过context方式向应用注入store,然后可以使用 connect 高阶方法,获取并监听store,然后根据store state和组件自身props计算得到新props,注入该组件,并且可以通过监听store,比较计算出的新props判断是否需要更新组件。</p> <p><img src="https://simg.open-open.com/show/8b4ea6b5526a6eaf0f03969b534e9717.png"></p> <h2>Provider</h2> <p>首先,react-redux库提供 Provider 组件将store注入整个React应用的某个入口组件,通常是应用的顶层组件。 Provider 组件使用context向下传递store:</p> <pre> <code class="language-javascript">// 内部组件获取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) } }</code></pre> <h3>Example</h3> <pre> <code class="language-javascript">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') )</code></pre> <h2>connect方法</h2> <p>在前面我们使用 Provider 组件将redux store注入应用,接下来需要做的是连接组件和store。而且我们知道Redux不提供直接操作store state的方式,我们只能通过其 getState 访问数据,或通过 dispatch 一个action来改变store state。</p> <p>这也正是react-redux提供的connect高阶方法所提供的能力。</p> <h3>Example</h3> <p>container/TodoList.js</p> <p>首先我们创建一个列表容器组件,在组件内负责获取todo列表,然后将todos传递给TodoList展示型组件,同时传递事件回调函数,展示型组件触发诸如点击等事件时,调用对应回调,这些回调函数内通过dispatch actions来更新redux store state,而最终将store和展示型组件连接起来使用的是react-redux的 connect 方法,该方法接收</p> <pre> <code class="language-javascript">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)</code></pre> <p>components/TodoList.js</p> <pre> <code class="language-javascript">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</code></pre> <p>components/Todo.js</p> <pre> <code class="language-javascript">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</code></pre> <h2>容器组件与展示型组件</h2> <p>在使用Redux作为React应用的状态管理容器时,通常贯彻将组件划分为容器组件(Container Components)和展示型组件(Presentational Components)的做法,</p> <table> <thead> <tr> <th> </th> <th>Presentational Components</th> <th>Container Components</th> </tr> </thead> <tbody> <tr> <td>目标</td> <td>UI展示 (HTML结构和样式)</td> <td>业务逻辑(获取数据,更新状态)</td> </tr> <tr> <td>感知Redux</td> <td>无</td> <td>有</td> </tr> <tr> <td>数据来源</td> <td>props</td> <td>订阅Redux store</td> </tr> <tr> <td>变更数据</td> <td>调用props传递的回调函数</td> <td>Dispatch Redux actions</td> </tr> <tr> <td>可重用</td> <td>独立性强</td> <td>业务耦合度高</td> </tr> </tbody> </table> <p>应用中大部分代码是在编写展示型组件,然后使用一些容器组件将这些展示型组件和Redux store连接起来。</p> <h2>connect()源码分析</h2> <p><img src="https://simg.open-open.com/show/175390e755759b699ced16065f3e77e7.png"></p> <pre> <code class="language-javascript">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 // 其他配置项 }); }</code></pre> <h3>strictEquall</h3> <pre> <code class="language-javascript">function strictEqual(a, b) { return a === b }</code></pre> <h3>shallowEquall</h3> <p>源码</p> <pre> <code class="language-javascript">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 }</code></pre> <pre> <code class="language-javascript">shallowEqual({x:{}},{x:{}}) // false shallowEqual({x:1},{x:1}) // true</code></pre> <h2>connectAdvanced高阶函数</h2> <p>源码</p> <pre> <code class="language-javascript">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) } }</code></pre> <h3>selectorFactory</h3> <p>selectorFactory 函数返回一个selector函数,根据store state, 展示型组件props,和dispatch计算得到新props,最后注入容器组件, selectorFactory 函数结构形如:</p> <pre> <code class="language-javascript">(dispatch, options) => (state, props) => ({ thing: state.things[props.thingId], saveThing: fields => dispatch(actionCreators.saveThing(props.thingId, fields)), })</code></pre> <p>注:redux中的state通常指redux store的state而不是组件的state,另此处的props为传入组件wrapperComponent的props。</p> <p>源码</p> <pre> <code class="language-javascript">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 ); }</code></pre> <p>pureFinalPropsSelectorFactory</p> <pre> <code class="language-javascript">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) } }</code></pre> <p>handleFirstCall</p> <pre> <code class="language-javascript">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 }</code></pre> <p>defaultMergeProps</p> <pre> <code class="language-javascript">export function defaultMergeProps(stateProps, dispatchProps, ownProps) { // 默认合并props函数 return { ...ownProps, ...stateProps, ...dispatchProps } }</code></pre> <p>handleSubsequentCalls</p> <pre> <code class="language-javascript">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 }</code></pre> <p>计算返回新props</p> <p>只要展示型组件自身props发生变更,则需要重新返回新合并props,然后更新容器组件,无论store state是否变更:</p> <pre> <code class="language-javascript">// 只有展示型组件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 }</code></pre> <p>计算返回stateProps</p> <p>通常容器组件props变更由store state变更推动,所以只有store state变更的情况较多,而且此处也正是使用Immutable时需要注意的地方: <em> 不要在 mapStateToProps 方法内使用 toJS() 方法。 </em></p> <p>当 mapStateToProps 两次返回的props对象未有变更时,不需要重新计算,直接返回之前合并得到的props对象即可,之后在selector追踪对象中比较两次selector函数返回值是否有变更时,将返回false,容器组件不会触发变更。</p> <p>因为对比多次mapStateToProps返回的结果时是使用浅比较,所以不推荐使用Immutable.toJS()方法,其每次均返回一个新对象,对比将返回false,而如果使用Immutable且其内容未变更,则会返回true,可以减少不必要的重新渲染。</p> <pre> <code class="language-javascript">// 只有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 }</code></pre> <h2>hoist-non-react-statics</h2> <p>类似Object.assign,将子组件的非React的静态属性或方法复制到父组件,React相关属性或方法不会被覆盖而是合并。</p> <pre> <code class="language-javascript">hoistStatics(Connect, WrappedComponent)</code></pre> <h2>Connect Component</h2> <p>真正的Connect高阶组件,连接redux store state和传入组件,即将store state映射到组件props,react-redux使用Provider组件通过context方式注入store,然后Connect组件通过context接收store,并添加对store的订阅:</p> <pre> <code class="language-javascript">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)) } }</code></pre> <h3>addExtraProps()</h3> <p>给props添加额外的props属性:</p> <pre> <code class="language-javascript">// 添加额外的props addExtraProps(props) { const withExtras = { ...props } if (renderCountProp) withExtras[renderCountProp] = this.renderCount++;// render 调用次数 if (this.propsMode && this.subscription) withExtras[subscriptionKey] = this.subscription return withExtras }</code></pre> <h3>初始化selector追踪对象initSelector</h3> <p>Selector,选择器,根据redux store state和组件的自身props,计算出将注入该组件的新props,并缓存新props,之后再次执行选择器时通过对比得出的props,决定是否需要更新组件,若props变更则更新组件,否则不更新。</p> <p>使用 initSelector 方法初始化selector追踪对象及相关状态和数据:</p> <pre> <code class="language-javascript">// 初始化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) }</code></pre> <p>makeSelectorStateful()</p> <p>创建selector追踪对象以追踪(tracking)selector函数返回结果:</p> <pre> <code class="language-javascript">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 }</code></pre> <h3>初始化订阅initSubscription</h3> <p>初始化监听/订阅redux store state:</p> <pre> <code class="language-javascript">// 初始化订阅 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) }</code></pre> <p>订阅类实现</p> <p>组件订阅store使用的订阅发布器实现:</p> <pre> <code class="language-javascript">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() } }</code></pre> <h3>订阅回调函数</h3> <p>订阅后执行的回调函数:</p> <pre> <code class="language-javascript">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() }</code></pre> <h3>其他生命周期方法</h3> <pre> <code class="language-javascript">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 }</code></pre> <h2>参考阅读</h2> <ol> <li><a href="/misc/goto?guid=4959717446998433794" rel="nofollow,noindex">React with redux</a></li> <li><a href="/misc/goto?guid=4959645786337460505" rel="nofollow,noindex">Smart and Dumb Components</a></li> <li><a href="/misc/goto?guid=4959754639730891703" rel="nofollow,noindex">React Redux Container Pattern</a></li> </ol> <p> </p> <p>来自: <a href="/misc/goto?guid=4959754639847878347" rel="nofollow,noindex">React-Redux分析</a></p> <p> </p>