多个组件都需要某个功能,而且这个功能和界面并没有关系,所以不能简单地抽取称一个新的组件。
如何构建更易于复用、更灵活的 React 高级组件,包含下面两种方式:
1、高阶组件的概念及应用
2、以函数为子组件的模式
一、高阶组件
- 是使用 react 的一种模式,用于增强现有组件的功能
- 一个高阶组件就是一个函数,这个函数接受一个组件作为输入,然后返回一个新的组件作为结果,而且,返回的新组件拥有了输入组件所不具有的功能。(这里提到的组件,不是组件实例,而是一个组件类,也可以是一个无状态组件的函数)
例子
import React from 'react'
function removeUserProp(WrappedComponent) {
return class WrappingComponent extends React.Component {
render () {
const { user, ...otherProps } = this.props
return <WrappedComponent {...otherProps} />
}
}
}
export default removeUserProp
- 定义的函数 removeUserProp就是我们定义的第一个高阶组件
- 参数 WrappedComponent: 代表一个组件类,这个函数返回一个新的组件,所做的事情和 WrappedComponent 一模一样,只是忽略名为 user 的 prop
使用时
const NewComponent = removeUserProp(SampleComponent)
- 定义高阶组件的意义
- 重用代码。很多组件都需要公用同样一个逻辑,把这部分逻辑提取出来,利用高阶组件的方式应用出去,就可以产生新的组件,对原有组件没有任何伤害
- 修改现有 React 组件的行为。有些现成的 React 组件并不是开发者自己开发的,来自于第三方,或者是自己开发的,但是不想去触碰组件内部逻辑。这时候可以通过一个独立于原有组件的函数,可以产生新的组件,对原有组件没有任何伤害。
高阶组件实现方式
根据返回的新组件和传入组件参数的关系,实现方式分为两大类:
1、代理方式的高阶组件
2、继承方式的高阶组件
1.代理方式的高阶组件
上面的 removeUserProp 例子就是一个代理方式的高阶组件,特点是返回的新组件类直接继承自 React.Component
- 代理方式的高阶组件,可以应用在下列场景中:
- 操纵 prop
- 代理类型高阶组件返回的新组件,渲染过程也被新组件的 render 函数控制,而 render 函数相当于一个代理,完全决定如何使用被包裹的组件。
- 在 render 函数中, this.props 包含新组件接收到的所有的 prop,高阶组件可以增减、删除、修改传递给包裹组件的 props 列表
- 上面的 removeUserProp 就是一个删除了特定 prop 的高阶组件的例子
- 访问 ref
- 这样的高阶组件用处: 只要获得了对被包裹组件的 ref 引用,就可以通过这个引用任意操纵一个组件的 DOM 元素。
- 但是 ref 的使用非常容易出问题
- 抽取状态
- react-redux 的 connect 函数
- connect 函数本身并不是高阶函数,该函数执行的结果是另一个函数,这个函数才是高阶组件
- 在傻瓜组件和容器组件的关系中,通常让傻瓜组件不要管理自己的状态,只要做一个无状态的组件就好,所有状态的管理都交给外面的容器组件,这个模式就是“抽取状态”
- 见下边 “抽取状态例子”
- 包装组件
- render 函数的 JSX 中完全可以引入其他的元素,甚至可以组合多个 React 组件
- 一个实用的例子是给组件添加样式
- 见 “包装组件例子”
- 操纵 prop
抽取状态例子
const doNothing = () => ({})
function connect (mapStateToProps=doNothing, mapDispatchToProps=doNothing) {
return function(WrappedComponent) {
class HOCComponent extends React.Component {
// 在这里定义 HOCComponent 的生命周期函数
}
HOCComponent.contextTypes = {
store: React.PropTypes.object
}
return HOCComponent
}
}
// 在 react-redux 中,这个 context 由 Provider 提供,在组件中我们通过 this.context.store 就可以访问到 Provider 提供的 store 实例
// HOCComponent 组件
constructor () {
super (...arguments)
this.onChange = this.onChange.bind(this)
this.store = {}
}
componentDidMount () {
this.context.store.subscribe(this.onChange)
}
componentWillUnmount () {
this.context.store.unsubscribe(this.onChange)
}
onChange () {
this.setState({})
}
render () {
const store = this.context.store
const newProps = {
...this.props,
...mapStateToProps(store.getState()),
...mapDispatchToProps(store.dispatch)
}
return <WrappedComponent {...newProps} />
}
// store.getState 函数: 得到 Redux Store 的状态
// store.dispatch 得到传递给 mapDispatchToProps 的 dispatch 方法
包装组件例子
const styleHOC = (WrappedComponent, style) => {
return class HOCComponent extends React.Component {
render () {
return (
<div style={stye}>
<WrappedComponent {...this.props} />
</div>
)
}
}
}
2.继承方式的高阶组件
继承方式的高阶组件采用继承关系关联作为参数的组件和返回的组件,加入传入的组件参数是 WrappedComponent, 那么返回的组件就直接继承自 WrappedComponent
用继承方式实现 removeUserProp 这个高阶组件
function removeUserProp (WrappedComponent) {
return class NewComponent extends WrappedComponent {
render () {
const { user, ...otherProps } = this.props
this.props = otherProps
return super.render()
}
}
}
- 代理方式和继承方式最大的区别,是使用被包裹组件的方式
- 在代理方式下, render 函数中的使用被包裹组件是通过 JSX 代码
return <WrappedComponent {...otherProps} />
- 在继承方式下,render 函数中渲染被包裹组件的代码如下
return super.render()
// 创造的新的组件继承自 WrappedComponent , 所以直接调用 super.render 就能够得到渲染出来的元素
-
代理方式下,WrappedComponent 经历了一个完整的生命周期,产生的新组件和参数组件是两个不同的组件,一次渲染,两个组件都要经历各自的生命周期
-
继承方式下,super.render 只是一个生命周期中的一个函数而已,两个组件合二为一,只有一个生命周期
-
继承方式的高阶组件可以应用下列场景:
- 操纵 props
- 继承方式的高阶组件,除了上面不安全的直接修改 this.props 方法,还可以利用 React.cloneElement 让组件重新绘制
- 实现过程复杂,唯一用得上的场景就是高阶组件需要根据参数组件 WrappedComponent 渲染结果来决定如何修改 props
- 操纵生命周期函数
- 因为继承方式的高阶函数返回的新组件继承了参数组件,所以可以重新定义任何一个 React 组件的生命周期
- 操纵 props
-
各方面看来,代理方式都要优于继承方式
-
优先考虑组合,然后才考虑继承
3.高阶组件的显示名
为了方便开发和维护,往往需要给高阶组件重新定义一个 “显示名”,不然在 debug 和日志中看到的组件名就会莫名奇妙。增加 “显示名” 的方式就是给高阶组件类的 displayName 赋上一个字符串类型的值
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || 'Component'
}
HOCComponent.displayName = `Connect(${getDisplayName(WrappedComponent)})`
4.曾经的 React Mixin
在 ES6 的 React 组件类定义方法中不能使用 Mixin, React 官方也很明确声明 Mixin 是应该被废弃的方法,这里就不再介绍了。
二、以函数为子组件
高阶组件对原组件的 props 有固化的要求。也就是说,能不能把一个高阶组件作用于某个组件,要先看这个组件是不是能够接受高阶组件传过来的 props。
'以函数为子组件’的模式就是为了克服高阶组件的这种局限而生的。在这种模式下,实现代码重用的不是一个函数,而是一个真正的 React 组件,这样的 React 组件有个特点,要求必须有个子组件的存在,而且这个子组件必须是一个函数。
在组件实例的声明周期函数中, this.props.children 引用的就是子组件,render 函数会直接把 this.props.children 当作函数来调用,得到的结果就可以作为 render 返回结果的一部分
高阶组件和以函数为子组件对比例子
// 高阶组件实现
const addUserProp = (WrappedComponent) => {
class WrappingComponent extends React.Component {
render () {
const newProps = {user: loggedinUser}
return <WrappedComponent {...this.props} {...newProps} />
}
}
return WrappingComponent
}
// 以函数为子组件
const loggedinUser = 'mock user'
class AddUserProp extends React.Component {
render () {
const user = loggedinUser
return this.props.children(user)
}
}
AddUserProp.propTypes = {
children: React.PropTypes.func.isRequired
}
// 把 user显示出来
<AddUserProp>
{ (user) => <div> { user } </div> }
</AddUserProp>
// 把 user 作为 prop 传递给一个接受 user名prop 的组件 Foo
<AddUserProp>
{ (user) => <Foo user={user} /> }
</AddUserProp>
// 子组件接受名为 currentUser 的 prop
<AddUserProp>
{ (user) => <Bar currentUser={user} /> }
</AddUserProp>
使用以函数为子组件的方式创建的 AddUserProp 的灵活之处在于它没有对被增强组件有任何 props 要求,只是传递一个参数过去,至于怎么使用,完全由作为子组件的函数决定
性能优化问题
‘以函数为子组件’模式可以让代码非常灵活,但是凡事都有优点和缺点,这种模式的缺点就是难以做性能优化
- 虽然这种模式有性能上的潜在问题,但是依然是个非常棒的模式。这种模式是性能和灵活性的一个恰当折中。
总结
- React 高级用法:高阶组件和‘以函数为子组件’
- 高阶组件:两种实现方式
- 代理方式:更加容易实现和控制
- 继承方式:唯一优点就是可以操纵特定组件的生命周期函数
- ‘以函数为子组件’,更加灵活,因为有函数介入,连接两个组件的方式可以更加自由。
该文章为总结 《深入浅出 React 和 Redux 》第六章