咱們來觀察一下剛寫下的這幾個組件,能夠輕易地發現它們有兩個重大的問題:html
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
傳給 WrappedComponent
。spa
可是每一個傳進去的組件須要 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
。