Simple Redux主要是教您Redux的核心概念。部分是为了好玩,但主要是为了帮助您避免Redux应用程序中不必要的重新渲染。
适用于具有使用Redux和React的经验的开发人员。在这里将不会学习如何使用actions, reducers, 或the connect功能。相反,你将从这里了解它们是如何工作的。
Simple Redux实现了redux和react-redux包中的代码。这包括createStore,combineReducers,connect,和Provider组件。
存储
在Redux中,您可以通过createStore使用reducer 调用来创建存储:
import { createStore } from 'redux' import reducer from './reducer'
const store = createStore(reducer)
|
createStore是一个包含store state(currentState)的闭包,并返回store方法:
// redux/createStore.js export default function createStore(reducer) { let currentState let listeners = []
const getState = () => currentState
const subscribe = listener => { listeners.push(listener) }
const dispatch = action => { currentState = reducer(currentState, action) listeners.forEach(l => l()) }
// ..
return { dispatch, subscribe, getState } }
|
可以看到dispatch使用一个action和先前状态调用存储 reducer以生成新状态。
// redux/createStore.js
export default function createStore(reducer) { // .. const dispatch = action => { currentState = reducer(currentState, action) // .. } // .. }
|
这就是reducer必须总是返回一个对象的原因。提醒一下,这就是reducer的样子:
// app/store/todosReducer.js
const initialState = { items: [] }
export default function todosReducer(state = initialState, action) { if (action.type === 'ADD_TODO') { return { ...state, items: [...state.items, action.payload] } } return state }
|
请注意,reducer state参数的默认值为initialState。初始化状态时使用此值:
// app / store / todosReducer.js
const initialState = { items: [] } export default function todosReducer( state = initialState, action){ // .. return state }
|
初始化状态是createStore通过分发dispatch具有唯一标识type的一个action, 这个唯一标识不会与reducer中的任何情况匹配的。
// redux/createStore.js
export default function createStore(reducer) { let currentState
const dispatch = action => { currentState = reducer(currentState, action) // .. }
dispatch({ type: '@@redux/INIT$' })
return { // .. } }
|
最初的dispatch调用currentState结果是undefined。因此当使用dispatchwith undefined作为第一个参数调用reducer时,它将返回默认initialState值来创建currentState:
// app/store/todosReducer.js
const initialState = { items: [] }
export default function todosReducer(state = initialState, action) { // .. return state }
|
这就是存储的全部 - 它非常简单!但是你可能使用多个reducer来创建存储,那是需要combineReducers的地方。combineReducers
大多数应用程序使用combineReducers实现多个reducers 函数,而不只能是使用单个根reducer:
// app/store/reducers.js
import { combineReducers } from 'redux' import todos from './todosReducer' import modal from './modalReducer'
const rootReducer = combineReducers({ todos, modal })
|
这会生成一个状态对象:store.getState() // => // { todos: { items: [] }, modal: { } }
|
combineReducers很简单。它返回一个函数(combination),它调用每个reducer与reducer生成的先前状态,以及action:
// redux/combineReducers.js
export default function combineReducers(reducers) { return function combination(state = {}, action) { let nextState = {}
Object.keys(reducers).forEach(key => { let reducer = reducers[key] let previousStateForKey = state[key]
nextState[key] = reducer(previousStateForKey, action) })
return nextState } }
|
这就是创建Simple Redux存储所需的所有代码。
下一部分是将存储连接到应用程序的代码。
将存储连接到React
React项目使用react-redux包将存储连接到React应用程序。
可以使用以下connect函数创建容器组件:
import { connect } from 'react-redux'
const ModalContainer = connect(mapStateToProps)(Modal)
|
为了使容器工作,您需要<Provider />基于容器的组件树中的某处呈现组件。
一个<Provider />组件提供存储,接收为prop:
import store from './store/store'
const App = () => ( <Provider store={store}> <ModalContainer /> </Provider> )
export default App
|
<Provider />使用React Context API将存储用于子组件:
// react-redux/Provider.js
import React, { Component } from 'React' import Context from './Context'
export default class Provider extends Component { constructor(props) { super(props) this.state = { storeState: props.store.getState() } }
// ..
render() { return ( <Context.Provider value={this.state}> {this.props.children} </Context.Provider> ) } }
|
如果您不熟悉上下文API,请阅读React文档子组件可以访问<Provider />组件状态,包含storeState值,访问方式是通过渲染一个Context.Consumer:
const ModalStatus = () => ( <Context.Consumer> {({ storeState }) => <p>Modal is: {storeState.modal.visible}</p>} </Context.Consumer> )
|
此实现的storeState值是静态的。为了要确保storeState值是最新的,<Provider />组件需要在存储状态更新时重新呈现。
当<Provider />组件安装时,它使用store subscribe方法订阅存储。记住subscribe会在存储的listeners数组中添加一个监听器(一个回调函数)。在dispatch计算新状态后会调用数组监听器中每个侦听器:// redux/createStore.js
export default function createStore(reducer) { // .. let listeners = []
// ..
const subscribe = listener => { listeners.push(listener) }
const dispatch = action => { // .. listeners.forEach(l => l()) }
// .. }
|
监听器回调会使用新的存储状态调用this.setState:
// react-redux/Provider.js
export default class Provider extends Component { // .. componentDidMount() { const store = this.props.store
store.subscribe(() => { this.setState({ storeState: store.getState() }) }) } // .. }
|
setState然后触发一个组件重新渲染,然后重新渲染每个<Provider />子组件(除非您使用componentShouldUpdate或某些其他方法对其进行编码)。因此,<Provider />的每个子组件都被重新渲染。
这就是为什么需要connect的地方。请记住,connect创建一个容器组件,将存储状态映射到包装组件的props。像这样的东西:const mapStateToProps = state => ({ visible: state.modal.visible })
export default connect(mapStateToProps)(Modal)
|
connect返回<Connect />,后者是渲染呈现其包装组件,<Connect />会为包装组件生成prop,这是通过使用<Context.Consumer />子函数中可用的存储状态调用mapStateToProps实现的。
低效的实现看起来像这样:
import React from 'react' import Context from './Context'
export default function connectHOC(mapStateToProps) { return function wrapWithConnect(WrappedComponent) { const Connect = () => { const renderWrappedComponent = ({ storeState }) => { const props = mapStateToProps(storeState) return <WrappedComponent {...props} /> } return <Context.Consumer>{renderWrappedComponent}</Context.Consumer> }
return Connect } }
|
通过此实现,在每次存储更新时,<Connect />组件都会重新呈现。在一个大型应用程序中,这可能是一个巨大的性能打击
Redux通过使用称为memoization的缓存技术改进了这个简单的实现。
缓存memoization
Memoization是一个用于缓存函数调用结果的奇特单词,Memoization用于避免重新计算昂贵的函数调用,例如渲染React组件树。memoization一个实现是通过保存最后一个函数调用的结果和参数来工作的。如果下一个函数调用与前一个调用具有相同的参数,则返回缓存的结果。如果参数不同,则函数计算并缓存新结果:
const memoizeFn = fn => { let prevResult let prevArg return arg => { if (prevArg === arg) { return prevResult } const result = fn(arg) prevArg = arg prevResult = result return result } }
const calculateResult = a => a * 2
const memoizedFn = memoizeFn(calculateResult) memoizedFn(1) // calls `calculateResult` memoizedFn(1) // returns cached arg memoizedFn(1) // returns cached arg memoizedFn(2) // calls `calculateResult`
|
当计算功能是确定性时,Memoization仅适用于函数,也就是说,指定相同的输入它总是返回相同的结果。例如,sum函数返回其参数总和的函数是确定性的:
const sum = (a, b) => a + b
|
非确定性函数的示例是用于计算当前时间的timeAgo函数Date.now。每次调用时,该函数都会返回不同的结果:
const timeAgo = t => Date.now() - t
|
Memoization的另一个要求是该函数必须没有副作用。副作用是对函数外部的应用程序状态的更改。例如,setTitle是设置document.title的函数,具有更改document.title值的副作用:
const setTitle = title => { document.title = title }
|
不产生任何副作用的确定性函数称为纯函数。纯函数对于memoization工作至关重要,这就是为什么redux教程如此严格,以保持你的reducer纯净。
大多数memoization实现使用相等性检查来确保参数没有改变。因此,要在Redux中进行memoization ,您需要了解JavaScript是如何比较值的。JavaScript值
JavaScript的有六种基本类型:String,Number,Boolean,Undefined,Symbol,和Null。
使用严格相等运算符(===)比较基本类型时,JavaScript会比较这些值。具有相同值的两个不同的字符串被认为彼此相等:
const str = 'some string' str === str // true str === 'some string' // true str === 'different string' // false
|
JavaScript中的其他数据类型是Object。JavaScript中所有不是原始值的都是一个对象(包括函数和数组)。
每个对象都有一个唯一的对象值,该值与对象的结构分开:
const obj1 = {prop1 : true } //具有唯一对象值 const obj2 = {prop1 : false} //具有唯一对象值
|
使用严格相等运算符(===)比较对象时,JavaScript会比较两个对象的对象值,而不是比较结构:
obj1 === obj1 // true obj2 === obj2 // true obj1 === obj2 // false
|
现在您已了解memoization和JavaScript值,您可以看到redux如何使用它来避免不必要的重新渲染。
优化Connect组件
到目前为止,你对Redux已经有一个很好的印象,但是魔鬼在细节上,Redux源代码的复杂性来自优化避免重新渲染呈现。
在本节结束时,您将了解优化的工作原理,这将使您更轻松地保持自己的代码优化。
请记住,在react-redux中connect函数创建一个<Connect />组件:呈现渲染来自mapStateToProps传递的props中的包装的组件。
原来实现是<Connect />每次更新时都会渲染一个新组件:
import React from 'react' import Context from './Context'
export default function connectHOC(mapStateToProps) { return function wrapWithConnect(WrappedComponent) { const Connect = () => { const renderWrappedComponent = ({ storeState }) => { let props = mapStateToProps(storeState) return <WrappedComponent {...props} /> } return <Context.Consumer>{renderWrappedComponent}</Context.Consumer> }
return Connect } }
|
使用这种原始实现,每次dispatch调用包装组件(及其所有子组件)将重新呈现。这很不好。如果mapStateToProps创建的props对象自上次渲染后没有更改,解决方案是停止对包装组件重新渲染。
Redux不能使用严格的equality(===)检查来确保新的props与之前的props相同,因为mapStateToProps每次调用它时都会返回一个新对象(带有一个新的对象值)。
相反,redux使用shallowEqual辅助函数。shallowEqual遍历每个属性并声明它严格等于前一个属性:
// react-redux/shallow-equal.js
export default function shallowEqual(objA, objB) { const keysA = Object.keys(objA) const keysB = Object.keys(objB)
for (let i = 0; i < keysA.length; i++) { if (objA[keysA[i]] !== objB[keysA[i]]) { return false } }
return true }
|
因此,当<Connect />组件重新渲染时,它可以调用mapStateToProps使用新状态,并用shallowEqual将先前的prop与新生成的prop进行比较:
const nextProps = mapStateToProps(state) const propsChanged = !shallowEqual(lastDerivedProps, nextProps)
|
要执行此计算,Redux需要引用以前的props。Redux使用memoization来执行此操作:
function makeDerivedPropsSelector(mapStateToProps) { let lastDerivedProps
return function selectDerivedProps(state) { lastState = state
const nextProps = mapStateToProps(state) const propsChanged = !shallowEqual(lastDerivedProps, nextProps)
if (propsChanged) { lastDerivedProps = nextProps }
return lastDerivedProps } }
|
selectDerivedProps选择器中创建<Connect />的构造函数:
注意:选择器是使用memoization的函数。您在redux生态系统中看到了很多这个术语。
export default function connect(mapStateToProps) { return function wrapWithConnect(WrappedComponent) { class Connect extends PureComponent { constructor(props) { super(props) this.selectDerivedProps = makeDerivedPropsSelector(mapStateToProps) } // .. } return Connect } }
|
如果之前的prop与新prop相等,selectDerivedProps 则返回前一个道具对象:function makeDerivedPropsSelector(mapStateToProps) { let lastDerivedProps
return function selectDerivedProps(state) { // ..
if (propsChanged) { lastDerivedProps = nextProps }
return lastDerivedProps } }
|
因此<Connect />可以使用严格相等运算符来检查prop是否发生了变化:
let derivedProps = this.selectDerivedProps(storeState)
if (derivedProps !== previousProps) { // do something }
|
如果组件prop已更改,<Connect />则应更新包装组件。如果prop没有改变,则<Connect />不应更新组件。
避免在React中重新渲染子节点的一种方法是返回在前一渲染中使用的相同元素。
如果组件不需要更新,<Connect />则再次使用memoization返回先前生成的呈现元素。这是用另一个选择器完成的selectChildElement。如果prop已更改,则选择器将重新渲染<WrapperComponent />:function makeChildElementSelector(WrappedComponent) { let lastChildProps let lastChildElement
return function selectChildElement(childProps) { if (childProps !== lastChildProps) { lastChildProps = childProps lastChildElement = <WrappedComponent {...childProps} /> } return lastChildElement } }
|
所以现在<Connect/>使用makeDerivedProps选择器计算新的prop。然后它用新的prop调用selectChildElement。如果props没有更改,则选择器将返回先前呈现的元素,并且包装的组件将不会重新呈现。完整它看起来像这样:// react-redux/connect.js
export default function connectHOC(mapStateToProps) { return function wrapWithConnect(WrappedComponent) { class Connect extends PureComponent { constructor(props) { super(props) this.selectDerivedProps = makeDerivedPropsSelector(mapStateToProps) this.selectChildElement = makeChildElementSelector(WrappedComponent) this.renderWrappedComponent = this.renderWrappedComponent.bind(this) }
renderWrappedComponent({ storeState }) { let derivedProps = this.selectDerivedProps(storeState) return this.selectChildElement(derivedProps) }
render() { return ( <Context.Consumer>{this.renderWrappedComponent}</Context.Consumer> ) } } return Connect } }
|
在原始react-redux代码中,makeDerivedProps选择器使用mapStateToProps,mapDispatchToProps,mergeProps,和<Connect />自身的prop,来计算prop。现在它的工作原理相同:如果生成新组件props的结果与之前的props相等,则返回前一个props对象。这意味着该selectChildElement函数可以返回先前渲染的组件,前提是if childProps === lastChildProps。
优化您的容器
这方面的主要内容是Redux基于严格的平等检查重新渲染。
如果你的mapStateToProps函数在每次计算时都返回一个新对象,那么shallow检查将失败,并且每次dispatch调用组件都会重新渲染:
const mapStateToProps = state => ({ childProps: { name: state.modal.name } })
|
相反,使用mapStateToProps原始值:
const mapStateToProps = state => ({ childName: state.modal.name })
|
或者,您可以使用选择器库(如Reselect)来创建已memoization的函数,如果它所依赖的值未更改,则始终返回相同的对象:const getName = state => state.modal.name
const childPropsSelector = createSelector( [getName], name => ({ name }) ) const mapStateToProps = state => ({ childProps: childPropsSelector(state) })
|
恭喜,您已经完成了Simple Redux演练的结束。在这一点上,你应该很好地理解Redux如何工作。
现在就出去,避免在你的应用程序中进行不必要的重新渲染!
点击标题见源码