一、什么是hoc
hoc(高阶组件)是react中的一种组织代码的手段,而不是一个api,这种设计模式可以复用在react组件中的代码与逻辑,因为一般来说react组件比较容易复用渲染函数,也就是主要负责html的输出.
高阶组件实际上是经过以一个包装函数返回的组件,这类函数接受react组件处理传入的组件,然后返回一个新的组件。
二、如何实现一个hoc
- 找出组件中复用的逻辑
- 创建适用于上方逻辑的函数
- 利用这个函数来创建一个组件
- 调用
2.1找出组件中复用的逻辑
- 组件创建
- 向服务器拉去数据
- 利用数据渲染组件
- 监听数据的变化
- 数据变化或者处罚修改的事件
- 利用变化后的数据再次渲染
- 组件销毁移除监听的数据源
三、函数约定
1.hoc函数不要讲多余的props传递给被包裹对的组件
hoc函数需要像透明的一样,经过他的包裹产生新的组件和传入前没有什么区别。这样做的目的在于,我们不需要考虑经过hoc函数对传入的组件进行了修改,那么套用这种hoc含糊是多次后返回的组件在使用的时候,不得不考虑这个组件带来的一些非预期行为。
2.hoc是函数!利用函数来最大化组合性
因为hoc是一个返回组件的函数,只要是函数可以做的事hoc同样可以做到,利用这一点,我们可以借用在使用react之前我们就学过的一些东西,例:定义一个高阶函数用于返回一个高阶组件,或者干脆是一个不接受任何参数的函数。
四、注意
1、不要在render方法中使用hoc
我们都知道react会调用render方法来渲染组件,当然react也会做一些额外的工作例如性能优化,在组件重新渲染的时候react会判断当前render返回的组件和未之前的组件是否相等 === 如果相等react会递归更新组件,反之他会彻底的卸载之前的旧的本案本来渲染当前的组件。因为diff算法使用组件标识来确定是否应该更新子树还是丢弃并挂载新子树,如果返回的组件与前一个组件相同,则递归更新新子树,如果不相等,则会完全卸载前一个子树,而重新挂载组件会导致该组件及其所有子组件的状态丢失。
2、记得复制静态方法
react的组件一般是继承React.Componment的子类.
不要忘记了一个类除了实力方法外还有静态方法,使用hoc我们对组件进行了一层包装会覆盖掉原来的静态方法:
class Demo extends React.Compoent{
render(){
return (
<div>{this.props.children}</div>
)
}
}
Demo.echo = function(){
console.log('hello world')
}
Demo.echo();
function DemoHoc(Wrap){
return class extends React.Componet{
render(){
return (
<Wrap>{'hello world'}</Wrap>
)
}
}
}
const App = DemoHoc(Demo);
App.echo();
解决方式
在hoc内部直接将原来组件的静态方法复制就可以了:
function DemoHoc(Wrap){
const myClass = class extens React.Componet{
render(){
return (
<Wrap>{'hello world'}</Wrap>
)
}
}
myClass.echo = Wrap.echo;
return myClass;
}
不过这样一来hoc中就需要知道被复制的静态方法名是什么,结合之前提到的灵活使用HOC我们可以让HOC接收方法参数名称:
function DemoHoc(Wrap,staticMethods=[]){ //默认给空数组
const myClass = class extends ReaCT.Componet{
redner(){
return (
<Wrap>{'hello world'}</Wrap>
)
}
}
for(const metyhodName of staticMethods){
myClass[methodsName] = Wrap[methodsName];
}
return myClass;
}
const App = DemoHoc(Demo,['echo']);
3.透传 ref
ref作为组件上的特殊属性,无法像普通的props那样被向下传递。
class Wraped extends React.Componet{
constructor(props){
super(props);
this.state = {
message:''
}
}
echo(){
this.setState({
message:'hell world'
})
}
render(){
return <div>{this.state.message}</div>
}
}
我们使用一个HOC包裹它:
function ExampleHoc(Wrap){
return class extends React.Compoent{
render(){
return <Wrap></Wrap>
}
}
}
const Example = ExampleHoc(Wraped);
现在我们把这个组件放入到App组件中进行渲染,并且使用ref来引用这个返回的组件,并试图调用它的echo方法:
const ref = React.createRef();
class App Extends React.Component{
handleEcho = () =>{
ref.current.echo();
}
render(){
return (
<div>
<Example ref={ref}></Example>
<button onClick={this.handleEcho}>echo</button>
</div>
)
}
}
但是当你点击按钮试图触发子组件的事件的时候他不起作用,系统报错没echo方法。实际上ref被绑定到了HOC返回的那个匿名类上,想要绑定到内部的组件中我们可以进行ref透传,默认的情况下ref是无法被进行向下 传递的因为ref是特殊的属性就和key一样不会被添加到props中,因此react提供了一个API来实现透传ref的这种需求。
这个api就是React.forwardRef. 这个方法接受一个函数返回一个组件,在这个函数中它可以读取到组件传入的ref,某种意义上React.forwardRef也相当于一个高阶组件:
const ReturnedCompoent =React.forwardRef((props,ref) =>{
//我们可以获取到在props中无法获取的ref属性了.
return; // 返回这个需要使用ref属性的组件
})
我们把这个api用在之前的HOC中:
function Example(Wrap){
class Inner extends React.Compoent{
render(){
const {forwardRef,...rest} = this.props;
return <Wrap ref={forwardedRef} {...rest}></Wrap> // 我们接收到props中被改名的ref然后绑定到ref上
}
}
return React.forwardRef((props,ref) =>{ //我们接收到ref然后给他改名称forwardedRef传入到props中
return <Inner {..props} forwardedRef={ref}></Inner>
})
}
这个时候在调用echo就没有问题了:
handleEcho = () =>{
ref.current.echo();
}
五、高阶组件的实现方式
1、属性代理
在外部通过属性传值的方法给被高阶组件加工后的组件传值的时候,值会传给高阶组件,而不是被加工组件,换句话说,this.props只能在高阶组件内取到值。如果想在被加工组件内通过this.props取值,需要高阶组件往被加工组件身上传值。这就是属性代理。
通过属性代理的方式,可以使高阶组件使用起来更加自由,做更多的事情。
在app组件内调用Home组件渲染时,传递一个值:
import React from 'react'
import Home from './components/home'
export default class App extends React.Component{
render(){
return (
<div>
{/* 在此向Home组件传值 */}
<Home user="haha"/>
</div>
)
}
}
在Home组件通过this.props进行接收,结果为空对象,没有接收到值:
// ... //
render(){
console.log(this.props) // 输出结果为:{}
return (
<div>我是组件</div>
)
}
// ... //
在高阶组件内通过this.props进行接收,接收成功:
//...
render(){
console.log(this.props) // 输出结果为:{user:"haha"}
return (
<Fragment>
{this.state.isHeader?<div>Header</div>:""}
<WrapperComponent />
</Fragment>
)
}
// ...
2.反向继承
在高阶组件中,因为返回的组件直接继承了被加工组件,所以可以直接通过this.props访问被加工组件的数据。
1.在被加工组件Home中,定义数据isHeader,来控制是否显示Header
import React,{Component} from 'react'
import layout from '../layout'
class Home extends Component{
constructor(){
super();
// 在此处定义自己的数据
this.state = {
isHeader:true
}
}
render(){
return (
<div>我是组件</div>
)
}
}
export default layout(Home);
- 在高阶组件中,直接通过this.state获取Home组件中的state数据。
import React,{ Fragment } from 'react
export default (WrapperComponent)=>{
return class extends WrapperComponent{
render(){
return (
<Fragment>
{/* 在此处访问被加工组件的数据,如果isHeader为true,则渲染Header */}
{this.state.isHeader?<div>Header</div>:""}
<WrapperComponent />
</Fragment>
)
}
}
}
高阶组件继承了Home组件的所有东西,自然可以通过this来访问被加工的组件的属性了。
高阶组件的super
在高阶组件中,通过反向继承被加工的组件之后,可以对被继承组件做很多操作,例如通过super访问被继承组件中的方法。
super渲染被继承组件
在高阶组件中,渲染被继承组件有两种方法:
1、通过标签名直接渲染:
export default (WrapperComponent)=>{
return class extends WrapperComponent{
render(){
return (
<div>
<WrapperComponent />
</div>
)
}
}
}
2.通过super.redner()调用被继承组件的redner函数进行渲染
export default (WrapperComponent)=>{
return class extends WrapperComponent{
render(){
return (
<div>
{super.render()}
</div>
)
}
}
}
通过super调用被继承组件的方法
1、被继承组件中定义方法和数据:
import React, { Component } from 'react'
import layout from '../layout'
class Home extends Component {
constructor() {
super();
this.state = {
componentName: "Home组件"
}
}
render() {
return (
<div>我是组件</div>
)
}
handleClick() {
alert(1);
}
componentDidMount() {
console.log("componentDidMount")
}
}
// 在导出组件的时候,用高阶组件进行加工,则外部获取的组件便是加工后的有Header的组件
export default layout(Home);
2、在高阶组件中通过super进行操作
import React,{ Fragment } from 'react'
export default (WrapperComponent)=>{
return class extends WrapperComponent{
render(){
console.log(this.props,"layout")
return (
<div>
{super.render()}
{/* 操作被继承组件的方法,点击后成功alert(1) */}
<button onClick={super.handleClick.bind(this)}>点击</button>
</div>
)
}
componentDidMount(){
console.log("abc") // 输出结果:abc
{/* 操作被继承组件的生命周期 */}
super.componentDidMount() // 输出结果:componentDidMount
}
}
}