使用 react已经有不短的时间了,不过一直没深入研究过,最近看到关于 react高阶组件的一篇文章,看了之后觉得是个不可多得的 zhuangbility的利器,自然不可轻易错过,遂深入了解了一番。
概述
高阶组件的定义
React 官网上对高阶组件的定义:
高阶部件是一种用于复用组件逻辑的高级技术,它并不是 React API的一部分,而是从React 演化而来的一种模式。
具体地说,高阶组件就是一个接收一个组件并返回另外一个新组件的函数。
相比于普通组件将 props 转化成界面UI,高阶组件将一个普通组件转化为另外一个组件。
功能
可以利用高阶组件来做的事情:
代码复用,逻辑抽象,抽离底层准备(bootstrap)代码 Props 更改 State 抽象和更改 渲染劫持
用法示例
基本用法
一个最简单的高阶组件(HOC) 示例如下:// HOCComponent.js import React from 'react' export default PackagedComponent => class HOC extends React.Component { render() { return (
Title
此文件导出了一个函数,此函数返回经过一个经过处理的组件,它接受一个参数 PackagedComponent,此参数就是将要被 HOC包装的普通组件,接受一个普通组件,返回另外一个新的组件,很符合高阶组件的定义。
此高阶组件的简单使用如下:// main.js import React from 'react' // (1) import HOCComponent from './HOCComponent' // (2) @HOCComponent class Main extends React.Component { render() { return() } } // (2) // 也可以将上面的 @HOCComponent换成下面这句 // const MainComponent = HOCComponent(Main) export default MainComponent main content
想要使用高阶组件,首先(1)将高阶组件导入,然后(2)使用此组件包装需要被包装的普通组件 Main,这里的@符号是 ES7中的decorator,写过Java或者其他静态语言的同学应该并不陌生,这实际上就是一个语法糖,可以使用 react-decorators 进行转换, 在这里相当于下面这句代码:
const MainComponent = HOCComponent(Main)
@HOCComponent完全可以换成上面那句,只不过需要注意的是,类不具有提升的能力,所以若是觉得上面那句顺眼换一下,那么在换过之后,还要将这一句的位置移到类Main定义的后面。
最后,导出的是被高阶组件处理过的组件 MainComponent
这样,就完成了一个普通组件的包装,可以在页面上将被包装过的组件显示出来了:import React from 'react' import { render } from 'react-dom' // 导入组件 import MainComponent from './main' render(, document.getElementById('root') )
页面显示如下:
可以使用 React Developer Tools查看页面结构:
可以看出,组件Main的外面包装了一层 HOC,有点类似于父组件和子组件,但很显然高阶组件并不等于父组件。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPsHtzeLQ6NKq16LS4rXE0ru146OsIEhPQ9XiuPa4373X1+m8/qOsztLDx7/JxNy74dPDtb2yu9a50ru0zqOsuabE3Ly8yvXJz8O7yrLDtLnYz7WjrLWryseyu8D709q198rUo6zOqsHLv+zL2bXYx/i31rP2xLO49sbVzajX6bz+tcTL+cr0tcRIT0O1vbXXysfExNK7uPajrM7Sw8e/ydLUuPjV4tCpIEhPQ7340NDD/MP7o7o8L3A+DQo8cHJlIGNsYXNzPQ=="brush:sql;"> // 获取传入的被包装的组件名称,以便为 HOC 进行命名 let getDisplayName = component => { return component.displayName || component.name || 'Component' } export default PackagedComponent => class HOC extends React.Component { // 这里的 displayName就指的是 HOC的显示名称,我们将它重新定义了一遍 // static被 stage-0 stage-1 和 stage-2所支持 static displayName = `HOC(${getDisplayName(PackagedComponent)})` render() { return (
Title
现在的页面结构:
可以看到,原先的HOC已经变成了 HOC(Main)了,这么做主要是利于我们的调试开发。
这里的HOC,可以看做是一个简单的为普通组件增加Title的高阶组件,但是很明显并不是所有的页面都只使用同一个标题,标题必须要可定制化才符合实际情况。
想做到这一点也很简单,那就是再为HOC组件的高阶函数增加一个 title参数,另外考虑到 柯里化 Curry函数和函数式编程,我们修改后的 HOC代码如下:
// HOCComponent.js // 增加了一个函数,这个函数存在一个参数,此参数就是要传入的`title` export default PackagedComponent => componentTitle => class HOC extends React.Component { static displayName = `HOC(${getDisplayName(PackagedComponent)})` render() { return (
{ componentTitle ? componentTitle : 'Title' }
使用方式如下:
// main.js // ...省略代码 const MainComponent = HOCComponent(Main)('首页') export default MainComponent
然后在页面上就可以看到效果了:
属性代理
因为 HOC是包裹在普通组件外面的一层高阶函数,所以任何要传入普通组件内的props 或者 state 首先都要经过 HOC,这就让 HOC拥有了提前对这些属性进行修改的能力。
更改 Props
对 Props 的更改操作包括 增、删、改、查,在修改和删除 Props的时候需要注意,除非特殊要求,否则最好不要影响到原本传递给普通组件的 Props
class HOC extends React.Component { static displayName = `HOC(${getDisplayName(PackagedComponent)})` render() { // 向普通组件增加了一个新的 `Props` const newProps = { summary: '这是内容' } return (
{ componentTitle ? componentTitle : 'Title' }
通过 refs 获取组件实例
普通组件如果带有一个 ref属性,当其通过 HOC的处理后,已经无法通过类似 this.refs.component的形式获取到这个普通组件了,只会得到一个被处理之后的组件,想要仍然获得原先的普通组件,需要对 ref进行处理,一种处理方法类似于 react-readux 中的 connect方法,如下:
// HOCComponnet.js ... export default PackagedComponent => componentTitle => class HOC extends React.Component { static displayName = `HOC(${getDisplayName(PackagedComponent)})` // 回调方法,当被包装组件渲染完毕后,调用被包装组件的 changeColor 方法 propc(wrapperComponentInstance) { wrapperComponentInstance.changeColor() } render() { // 改变 props,使用 ref 获取被包装组件的示例,以调用其中的方法 const props = Object.assign({}, this.props, {ref: this.propc.bind(this)}) return (
{ componentTitle ? componentTitle : 'Title' }
使用:
// main.js ... class Main extends React.Component { render() { return() } // main.js 中的changeColor 方法 changeColor() { console.log(666); document.querySelector('p').style.color = 'greenyellow' } } ... main content
{ this.props.summary }
基于以上,我想到了一种可能,既然高阶组件能够代理到 普通组件的Props 和 state等属性,那么在使用诸如 redux等库的时候,是不是可以让高阶组件来承接这些由 redux传递到全局的属性,然后再用高阶组件包装普通组件,将获得的属性传递给普通组件,这样普通组件就能获取到 这些全局属性了。
相比于使用 redux一个个地初始化所有需要使用到全局属性的组件,使用高阶组件作为载体,虽然结构上多了一层,但是操作上明显方便简化了许多,我还没有验证过,有机会的话再试试。
反向继承(Inheritance Inversion)
说的通俗点,相比于前面使用 HOC包装在 普通组件外面的情况,反向继承就是让HOC继承普通组件、打入普通组件的内部,这种情况下,普通组件变成了基类,而HOC变成了子类,子类能够获得父类所有公开的方法和字段。
反向继承高阶组件的功能:
能够对普通组件生命周期内的所有钩子函数进行覆写 对普通组件的 state进行增删改查的操作。
// HOCInheritance.js let getDisplayName = (component)=> { return component.displayName || component.name || 'Component' } // (1) export default WrapperComponent => class Inheritance extends WrapperComponent { static displayName = `Inheritance(${getDisplayName(WrapperComponent)})` // (2) componentWillMount() { this.state.name = 'zhangsan' this.state.age = 18 } render() { // (4) return super.render() } componentDidMount() { // 5 super.componentDidMount() // 6 document.querySelector('h1').style.color = 'indianred' } }
上述代码中,让 Inheritance 继承 WrapperComponent (1),
并且覆写了WrapperComponent 中的 componentWillMount函数(2),
在这个方法中对 WrapperComponent 的 state进行操作(3),
在 render方法中,为了防止破坏WrapperComponent原有的 render()方法,使用 super将 WrapperComponent 中原有的 render方法实现了一次(4),
在 componentDidMount同样是先将 WrapperComponent 中的 componentDidMount方法实现了一次(5),
并且在原有的基础上,又进行了一些操作(6)
需要注意的是,super并不是必须使用,这取决于你是否需要实现普通组件中原有的对应函数,一般来说都是需要的,类似于 mixin,至于到底是原有钩子函数中的代码先执行,还是 HOC中另加的代码先执行,则取决于 super的位置,如果super在新增代码之上,则原有代码先执行,反之亦然。
另外,如果普通组件并没有显性实现某个钩子函数,然后在HOC中又添加了这个钩子函数,则 super不可用,因为并没有什么可以 super的,否则将报错。
使用:
// main2.js import React from 'react' import Inheritance from './HOCInheritance' class Main2 extends React.Component { state = { name: 'wanger' } render() { return () } componentDidMount() { document.querySelector('h1').innerHTML += this.state.name } } const InheritanceInstace = Inheritance(Main2) export default InheritanceInstace summary of
my name is {this.state.name}, I'm {this.state.age}
页面效果:
可以看出,HOC为原有组件添加了 componentWillMount函数,在其中覆盖了 Main2中 state的 ‘name’属性,并且其上添加了一个age属性
HOC还将 Main的 componentDidMount方法实现了一次,并且在此基础上,实现了自己的 componentDidMount方法。
注意事项
react官网 上还给出了几条关于使用 HOC 时的注意事项。
不要在render函数中使用高阶组件例如,以下就是错误示范:
// 这是个 render 方法 render() { // 在 render 方法中使用了 HOC // 每一次render函数调用都会创建一个新的EnhancedComponent实例 // EnhancedComponent1 !== EnhancedComponent2 const EnhancedComponent = enhance(MyComponent); // 每一次都会使子对象树完全被卸载或移除 return静态方法必须复制; }
HOC 虽然可以自动获得 普通组件的 props和 state等属性,但静态方法必须要手动挂载。
// 定义静态方法 WrappedComponent.staticMethod = function() {/*...*/} // 使用高阶组件 const EnhancedComponent = enhance(WrappedComponent); // 增强型组件没有静态方法 typeof EnhancedComponent.staticMethod === 'undefined' // true
为了解决这个问题,在返回之前,可以向容器组件中复制原有的静态方法:
function enhance(WrappedComponent) { class Enhance extends React.Component {/*...*/} // 必须得知道要拷贝的方法 Enhance.staticMethod = WrappedComponent.staticMethod; return Enhance; }
或者使用 hoist-non-react-statics来自动复制这些静态方法
Refs不会被传递对于 react组件来说,ref其实不是一个属性,就像key一样,尽管向其他props一样传递到了组件中,但实际上在组件内时获取不到的,它是由React特殊处理的。如果你给高阶组件产生的组件的元素添加 ref,ref引用的是外层的容器组件的实例,而不是被包裹的组件。
想要解决这个问题,首先是尽量避免使用 ref,如果避免不了,那么可以参照本文上面提到过的方法。