動手實現 React-redux(三):connect 和 mapStateToProps

咱們來觀察一下剛寫下的這幾個組件,能夠輕易地發現它們有兩個重大的問題:html

  1. 有大量重複的邏輯:它們基本的邏輯都是,取出 context,取出裏面的 store,而後用裏面的狀態設置本身的狀態,這些代碼邏輯其實都是相同的。
  2. 對 context 依賴性過強:這些組件都要依賴 context 來取數據,使得這個組件複用性基本爲零。想一下,若是別人須要用到裏面的 ThemeSwitch 組件,可是他們的組件樹並無 context 也沒有 store,他們無法用這個組件了。

對於第一個問題,咱們在 高階組件 的章節說過,能夠把一些可複用的邏輯放在高階組件當中,高階組件包裝的新組件和原來組件之間經過 props 傳遞信息,減小代碼的重複程度。react

對於第二個問題,咱們得弄清楚一件事情,到底什麼樣的組件才叫複用性強的組件。若是一個組件對外界的依賴過於強,那麼這個組件的移植性會不好,就像這些嚴重依賴 context 的組件同樣。編程

若是一個組件的渲染只依賴於外界傳進去的 props 和本身的 state,而並不依賴於其餘的外界的任何數據,也就是說像純函數同樣,給它什麼,它就吐出(渲染)什麼出來。這種組件的複用性是最強的,別人使用的時候根本不用擔憂任何事情,只要看看 PropTypes 它能接受什麼參數,而後把參數傳進去控制它就好了。redux

咱們把這種組件叫作 Pure Component,由於它就像純函數同樣,可預測性很是強,對參數(props)之外的數據零依賴,也不產生反作用。這種組件也叫 Dumb Component,由於它們呆呆的,讓它幹啥就幹啥。寫組件的時候儘可能寫 Dumb Component 會提升咱們的組件的可複用性。app

到這裏思路慢慢地變得清晰了,咱們須要高階組件幫助咱們從 context 取數據,咱們也須要寫 Dumb 組件幫助咱們提升組件的複用性。因此咱們儘可能多地寫 Dumb 組件,而後用高階組件把它們包裝一層,高階組件和 context 打交道,把裏面數據取出來經過 props 傳給 Dumb 組件。函數

咱們把這個高階組件起名字叫 connect,由於它把 Dumb 組件和 context 鏈接(connect)起來了:this

import React, { Component } from 'react'
import PropTypes from 'prop-types'

export connect = (WrappedComponent) => {
  class Connect extends Component {
    static contextTypes = {
      store: PropTypes.object
    }

    // TODO: 如何從 store 取數據?

    render () {
      return <WrappedComponent />
    }
  }

  return Connect
}

connect 函數接受一個組件 WrappedComponent 做爲參數,把這個組件包含在一個新的組件 Connect 裏面,Connect 會去 context 裏面取出 store。如今要把 store 裏面的數據取出來經過 props 傳給 WrappedComponentspa

可是每一個傳進去的組件須要 store 裏面的數據都不同的,因此除了給高階組件傳入 Dumb 組件之外,還須要告訴高級組件咱們須要什麼數據,高階組件才能正確地去取數據。爲了解決這個問題,咱們能夠給高階組件傳入相似下面這樣的函數:設計

const mapStateToProps = (state) => {
  return {
    themeColor: state.themeColor,
    themeName: state.themeName,
    fullName: `${state.firstName} ${state.lastName}`
    ...
  }
}

這個函數會接受 store.getState() 的結果做爲參數,而後返回一個對象,這個對象是根據 state 生成的。mapStateTopProps 至關於告知了 Connect 應該如何去 store 裏面取數據,而後能夠把這個函數的返回結果傳給被包裝的組件:code

import React, { Component } from 'react'
import PropTypes from 'prop-types'

export const connect = (mapStateToProps) => (WrappedComponent) => {
  class Connect extends Component {
    static contextTypes = {
      store: PropTypes.object
    }

    render () {
      const { store } = this.context
      let stateProps = mapStateToProps(store.getState())
      // {...stateProps} 意思是把這個對象裏面的屬性所有經過 `props` 方式傳遞進去
      return <WrappedComponent {...stateProps} />
    }
  }

  return Connect
}

connect 如今是接受一個參數 mapStateToProps,而後返回一個函數,這個返回的函數纔是高階組件。它會接受一個組件做爲參數,而後用 Connect 把組件包裝之後再返回。 connect 的用法是:

...
const mapStateToProps = (state) => {
  return {
    themeColor: state.themeColor
  }
}
Header = connect(mapStateToProps)(Header)
...

有些朋友可能會問爲何不直接 const connect = (mapStateToProps, WrappedComponent),而是要額外返回一個函數。這是由於 React-redux 就是這麼設計的,而我的觀點認爲這是一個 React-redux 設計上的缺陷,這裏有機會會在關於函數編程的章節再給你們科普,這裏暫時不深究了。

咱們把上面 connect 的函數代碼單獨分離到一個模塊當中,在 src/ 目錄下新建一個 react-redux.js,把上面的 connect 函數的代碼複製進去,而後就能夠在 src/Header.js 裏面使用了:

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from './react-redux'

class Header extends Component {
  static propTypes = {
    themeColor: PropTypes.string
  }

  render () {
    return (
      <h1 style={{ color: this.props.themeColor }}>React.js 小書</h1>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    themeColor: state.themeColor
  }
}
Header = connect(mapStateToProps)(Header)

export default Header

能夠看到 Header 刪掉了大部分關於 context 的代碼,它除了 props 什麼也不依賴,它是一個 Pure Component,而後經過 connect 取得數據。咱們不須要知道 connect 是怎麼和 context 打交道的,只要傳一個 mapStateToProps 告訴它應該怎麼取數據就能夠了。一樣的方式修改 src/Content.js

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ThemeSwitch from './ThemeSwitch'
import { connect } from './react-redux'

class Content extends Component {
  static propTypes = {
    themeColor: PropTypes.string
  }

  render () {
    return (
      <div>
        <p style={{ color: this.props.themeColor }}>React.js 小書內容</p>
        <ThemeSwitch />
      </div>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    themeColor: state.themeColor
  }
}
Content = connect(mapStateToProps)(Content)

export default Content

connect 尚未監聽數據變化而後從新渲染,因此如今點擊按鈕只有按鈕會變顏色。咱們給 connect 的高階組件增長監聽數據變化從新渲染的邏輯,稍微重構一下 connect

export const connect = (mapStateToProps) => (WrappedComponent) => {
  class Connect extends Component {
    static contextTypes = {
      store: PropTypes.object
    }

    constructor () {
      super()
      this.state = { allProps: {} }
    }

    componentWillMount () {
      const { store } = this.context
      this._updateProps()
      store.subscribe(() => this._updateProps())
    }

    _updateProps () {
      const { store } = this.context
      let stateProps = mapStateToProps(store.getState(), this.props) // 額外傳入 props,讓獲取數據更加靈活方便
      this.setState({
        allProps: { // 整合普通的 props 和從 state 生成的 props
          ...stateProps,
          ...this.props
        }
      })
    }

    render () {
      return <WrappedComponent {...this.state.allProps} />
    }
  }

  return Connect
}

咱們在 Connect 組件的 constructor 裏面初始化了 state.allProps,它是一個對象,用來保存須要傳給被包裝組件的全部的參數。生命週期 componentWillMount 會調用調用 _updateProps 進行初始化,而後經過 store.subscribe 監聽數據變化從新調用 _updateProps

爲了讓 connect 返回新組件和被包裝的組件使用參數保持一致,咱們會把全部傳給 Connect 的 props 原封不動地傳給 WrappedComponent。因此在 _updateProps 裏面會把 stateProps 和 this.props 合併到 this.state.allProps 裏面,再經過 render 方法把全部參數都傳給 WrappedComponent

mapStateToProps 也發生點變化,它如今能夠接受兩個參數了,咱們會把傳給 Connect 組件的 props 參數也傳給它,那麼它生成的對象配置性就更強了,咱們能夠根據 store 裏面的 state 和外界傳入的 props 生成咱們想傳給被包裝組件的參數。

如今已經很不錯了,Header.js 和 Content.js 的代碼都大大減小了,而且這兩個組件 connect 以前都是 Dumb 組件。接下來會繼續重構 ThemeSwitch

相關文章
相關標籤/搜索