咱们来观察一下刚写下的这几个组件,能够轻易地发现它们有两个重大的问题: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
。