我們來觀察一下剛寫下的這幾個組件,可以輕易地發現它們有兩個重大的問題:
- 有大量重復的邏輯:它們基本的邏輯都是,取出 context,取出里面的 store,然后用里面的狀態設置自己的狀態,這些代碼邏輯其實都是相同的。
- 對 context 依賴性過強:這些組件都要依賴 context 來取數據,使得這個組件復用性基本為零。想一下,如果別人需要用到里面的
ThemeSwitch
組件,但是他們的組件樹並沒有 context 也沒有 store,他們沒法用這個組件了。
對於第一個問題,我們在 高階組件 的章節說過,可以把一些可復用的邏輯放在高階組件當中,高階組件包裝的新組件和原來組件之間通過 props
傳遞信息,減少代碼的重復程度。
對於第二個問題,我們得弄清楚一件事情,到底什么樣的組件才叫復用性強的組件。如果一個組件對外界的依賴過於強,那么這個組件的移植性會很差,就像這些嚴重依賴 context 的組件一樣。
如果一個組件的渲染只依賴於外界傳進去的 props
和自己的 state
,而並不依賴於其他的外界的任何數據,也就是說像純函數一樣,給它什么,它就吐出(渲染)什么出來。這種組件的復用性是最強的,別人使用的時候根本不用擔心任何事情,只要看看 PropTypes
它能接受什么參數,然后把參數傳進去控制它就行了。
我們把這種組件叫做 Pure Component,因為它就像純函數一樣,可預測性非常強,對參數(props
)以外的數據零依賴,也不產生副作用。這種組件也叫 Dumb Component,因為它們呆呆的,讓它干啥就干啥。寫組件的時候盡量寫 Dumb Component 會提高我們的組件的可復用性。
到這里思路慢慢地變得清晰了,我們需要高階組件幫助我們從 context 取數據,我們也需要寫 Dumb 組件幫助我們提高組件的復用性。所以我們盡量多地寫 Dumb 組件,然后用高階組件把它們包裝一層,高階組件和 context 打交道,把里面數據取出來通過 props
傳給 Dumb 組件。
我們把這個高階組件起名字叫 connect
,因為它把 Dumb 組件和 context 連接(connect)起來了:
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
。
但是每個傳進去的組件需要 store 里面的數據都不一樣的,所以除了給高階組件傳入 Dumb 組件以外,還需要告訴高級組件我們需要什么數據,高階組件才能正確地去取數據。為了解決這個問題,我們可以給高階組件傳入類似下面這樣的函數:
const mapStateToProps = (state) => { return { themeColor: state.themeColor, themeName: state.themeName, fullName: `${state.firstName} ${state.lastName}` ... } }
這個函數會接受 store.getState()
的結果作為參數,然后返回一個對象,這個對象是根據 state
生成的。mapStateTopProps
相當於告知了 Connect
應該如何去 store 里面取數據,然后可以把這個函數的返回結果傳給被包裝的組件:
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
。