通过simple-redux理解React.js的Redux工作原理

19-01-06 banq
                   

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如何工作。

现在就出去,避免在你的应用程序中进行不必要的重新渲染!

点击标题见源码

                   

1