react 进阶之高阶组件

发布于 2022-10-23 11:09:28 字数 12482 浏览 189 评论 18

本文属于 react 进阶用法,如果你还不了解react,建议从文档开始看起。我们都知道高阶函数是什么, 高阶组件其实是差不多的用法,只不过传入的参数变成了react组件,并返回一个新的组件。

A higher-order component is a function that takes a component and returns a new component.

形如:

const EnhancedComponent = higherOrderComponent(WrappedComponent);

高阶组件是 react 应用中很重要的一部分,最大的特点就是重用组件逻辑。它并不是由 React API 定义出来的功能,而是由 React 的组合特性衍生出来的一种设计模式。如果你用过 redux,那你就一定接触过高阶组件,因为 react-redux 中的 connect。

就是一个高阶组件。

另外本次 demo 代码都放在 https://github.com/sunyongjian/hoc-demo

引入

先来一个最简单的高阶组件

import React, { Component } from 'react';
import simpleHoc from './simple-hoc';

class Usual extends Component {
  render() {
    console.log(this.props, 'props');
    return (
      <div>
        Usual
      </div>
    )
  }
}
export default simpleHoc(Usual);
import React, { Component } from 'react';

const simpleHoc = WrappedComponent => {
  console.log('simpleHoc');
  return class extends Component {
    render() {
      return <WrappedComponent {...this.props}/>
    }
  }
}
export default simpleHoc;

组件 Usual 通过 simpleHoc 的包装,打了一个 log...,那么形如 simpleHoc 就是一个高阶组件了,通过接收一个组件 class Usual,并返回一个组件 class。 其实我们可以看到,在这个函数里,我们可以做很多操作。 而且 return 的组件同样有自己的生命周期,function,另外,我们看到也可以把 props 传给 WrappedComponent(被包装的组件)。 高阶组件的定义我都是用箭头函数去写的,如有不适请参照 arrow function

装饰器模式

高阶组件可以看做是装饰器模式(Decorator Pattern)在 React 的实现。即允许向一个现有的对象添加新的功能,同时又不改变其结构,属于包装模式(Wrapper Pattern)的一种,ES7 中添加了一个 decorator 的属性,使用 @ 符表示,可以更精简的书写。那上面的例子就可以改成:

import React, { Component } from 'react';
import simpleHoc from './simple-hoc';

@simpleHoc
export default class Usual extends Component {
  render() {
    return (
      <div>
        Usual
      </div>
    )
  }
}

是同样的效果。当然兼容性是存在问题的,通常都是通过 babel 去编译的。 babel 提供了 plugin,高阶组件用的是类装饰器,所以用 transform-decorators-legacy babel

两种形式

属性代理

引入里我们写的最简单的形式,就是属性代理(Props Proxy)的形式。通过 hoc 包装 wrappedComponent,也就是例子中的 Usual,本来传给 Usual 的 props,都在 hoc 中接受到了,也就是 props proxy。 由此我们可以做一些操作

操作 props

最直观的就是接受到props,我们可以做任何读取,编辑,删除的很多自定义操作。包括hoc中定义的自定义事件,都可以通过props再传下去。

import React, { Component } from 'react';

const propsProxyHoc = WrappedComponent => class extends Component {

  handleClick() {
    console.log('click');
  }

  render() {
    return (<WrappedComponent
      {...this.props}
      handleClick={this.handleClick}
    />);
  }
};
export default propsProxyHoc;

然后我们的 Usual 组件 render 的时候, console.log(this.props) 会得到 handleClick.

refs 获取组件实例

当我们包装 Usual 的时候,想获取到它的实例怎么办,可以通过引用(ref),在 Usual 组件挂载的时候,会执行 ref 的回调函数,在 hoc 中取到组件的实例。通过打印,可以看到它的 props, state,都是可以取到的。

import React, { Component } from 'react';

const refHoc = WrappedComponent => class extends Component {

  componentDidMount() {
    console.log(this.instanceComponent, 'instanceComponent');
  }

  render() {
    return (<WrappedComponent
      {...this.props}
      ref={instanceComponent => this.instanceComponent = instanceComponent}
    />);
  }
};

export default refHoc;

抽离 state

这里不是通过 ref 获取 state, 而是通过 { props 回调函数 } 传递给 wrappedComponent 组件,通过回调函数获取 state。这里用的比较多的就是 react 处理表单的时候。通常 react 在处理表单的时候,一般使用的是受控组件(文档),即把input都做成受控的,改变 value 的时候,用 onChange 事件同步到 state 中。当然这种操作通过 Container 组件也可以做到,具体的区别放到后面去比较。看一下代码就知道怎么回事了:

// 普通组件Login
import React, { Component } from 'react';
import formCreate from './form-create';
  
@formCreate
export default class Login extends Component {
  render() {
    return (
      <div>
        <div>
          <label>
            账户
          </label>
          <input name="username" {...this.props.getField('username')}/>
        </div>
        <div>
          <label>
            密码
          </label>
          <input name="password" {...this.props.getField('password')}/>
        </div>
        <div onClick={this.props.handleSubmit}>提交</div>
        <div>other content</div>
      </div>
    )
  }
}
//HOC
import React, { Component } from 'react';

const formCreate = WrappedComponent => class extends Component {

  constructor() {
    super();
    this.state = {
      fields: {},
    }
  }

  onChange = key => e => {
    this.setState({
      fields: {
        ...this.state.fields,
        [key]: e.target.value,
      }
    })
  }

  handleSubmit = () => {
    console.log(this.state.fields);
  }

  getField = fieldName => {
    return {
      onChange: this.onChange(fieldName),
    }
  }

  render() {
    const props = {
      ...this.props,
      handleSubmit: this.handleSubmit,
      getField: this.getField,
    }

    return (<WrappedComponent
      {...props}
    />);
  }
};
export default formCreate;

这里我们把 state,onChange 等方法都放到 HOC 里,其实是遵从的 react 组件的一种规范,子组件简单,傻瓜,负责展示,逻辑与操作放到 Container。比如说我们在 HOC 获取到用户名密码之后,再去做其他操作,就方便多了,而 state,处理函数放到 Form 组件里,只会让 Form 更加笨重,承担了本不属于它的工作,这样我们可能其他地方也需要用到这个组件,但是处理方式稍微不同,就很麻烦了。

反向继承

反向继承(Inheritance Inversion),简称II,本来我是叫继承反转的...因为有个模式叫控制反转嘛...,跟属性代理的方式不同的是,II 采用通过 去继承 WrappedComponent,本来是一种嵌套的关系,结果II返回的组件却继承了 WrappedComponent,这看起来是一种反转的关系。

通过继承 WrappedComponent,除了一些静态方法,包括生命周期,state,各种 function,我们都可以得到。上栗子:

 // usual
import React, { Component } from 'react';
import iiHoc from './ii-hoc';

@iiHoc
export default class Usual extends Component {
  
  constructor() {
    super();
    this.state = {
      usual: 'usual',
    }
  }

  componentDidMount() {
    console.log('didMount')
  }

  render() {
    return (
      <div>
        Usual
      </div>
    )
  }
}
//IIHOC
import React from 'react';

const iiHoc = WrappedComponent => class extends WrappedComponent {
    render() {
      console.log(this.state, 'state');
      return super.render();
    }
}

export default iiHoc;

iiHoc return 的组件通过继承,拥有了 Usual 的生命周期及属性,所以 didMount 会打印,state 也通过 constructor 执行,得到 state.usual。
其实,你还可以通过II:

渲染劫持

这里 HOC 里定义的组件继承了 WrappedComponent 的 render(渲染),我们可以以此进行 hijack(劫持),也就是控制它的 render 函数。栗子:

//hijack-hoc
import React from 'react';

const hijackRenderHoc = config => WrappedComponent => class extends WrappedComponent {
  render() {
    const { style = {} } = config;
    const elementsTree = super.render();
    console.log(elementsTree, 'elementsTree');
    if (config.type === 'add-style') {
      return <div style={{...style}}>
        {elementsTree}
      </div>;
    }
    return elementsTree;
  }
};

export default hijackRenderHoc;
//usual
@hijackRenderHoc({type: 'add-style', style: { color: 'red'}})
class Usual extends Component {
  ...
}

我这里通过二阶函数,把 config 参数预制进 HOC, 算是一种柯理化的思想。栗子很简单,这个 hoc 就是添加样式的功能。但是它暴露出来的信息却不少。首先我们可以通过 config 参数进行逻辑判断,有条件的渲染,当然这个参数的作用很多,react-redux 中的 connect 不就是传入了props-key 嘛。再就是我们还可以拿到 WrappedComponent 的元素树,可以进行修改操作。最后就是我们通过div包裹,设置了 style。但其实具体如何操作还是根据业务逻辑去处理的...

element-tree

我的应用场景

通常我会通过高阶组件去优化之前老项目写的不好的地方,比如两个页面 UI 几乎一样,功能几乎相同,仅仅几个操作不太一样,却写了两个耦合很多的页面级组件。当我去维护它的时候,由于它的耦合性过多,经常会添加一个功能(这两个组件都要添加),我要去改完第一个的时候,还要改第二个。而且有时候由于我的记性不好,会忘掉第二个... 就会出现bug再返工。更重要的是由于个人比较懒,不想去重构这部分的代码,因为东西太多了,花费太多时间。所以加新功能的时候,我会写一个高阶组件,往HOC里添加方法,把那两个组件包装一下,也就是属性代理。这样新代码就不会再出现耦合,旧的逻辑并不会改变,说不定哪天心情好就会抽离一部分功能到 HOC 里,直到理想的状态。

另一种情况就是之前写过一个组件A,做完上线,之后产品加了一个新需求,很奇怪要做的组件B跟A几乎一模一样,但稍微有区别。那我可能就通过II的方式去继承之前的组件 A,比如它在 didMount 去 fetch 请求,需要的数据是一样的。不同的地方我就会放到 HOC 里,存储新的 state 这样,再通过劫持渲染,把不同的地方,添加的地方进行处理。但其实这算 Hack 的一种方式,能快速解决问题,也反映了组件设计规划之初有所不足(原因比较多)。

Container 解决不了的时候甚至不太优雅的时候。其实大部分时候包一层 Container 组件也能做到差不多的效果,比如操作 props,渲染劫持。但其实还是有很大区别的。比如我们现在有两个功能的container,添加样式和添加处理函数的,对 Usual 进行包装。栗子:

//usual
class Usual extends Component {

  render() {
    console.log(this.props, 'props');
    return <div>
      Usual
    </div>
  }
};
export default Usual;
//console - Object {handleClick: function}  "props"
import React, { Component } from 'react';
import Usual from './usual';

class StyleContainer extends Component {

  render() {
    return (<div style={{ color: '#76d0a3' }}>
      <div>container</div>
      <Usual {...this.props} />
    </div>);
  }
}

export default StyleContainer;
import React, { Component } from 'react';
import StyleContainer from './container-add-style';

class FuncContainer extends Component {
  handleClick() {
    console.log('click');
  }

  render() {
    const props = {
      ...this.props,
      handleClick: this.handleClick,
    };
    return (<StyleContainer {...props} />);
  }
}

export default FuncContainer;

外层 Container 必须要引入内层 Container,进行包装,还有 props 的传递,同样要注意包装的顺序。当然你可以把所有的处理都放到一个 Container 里。那用 HOC 怎么处理呢,相信大家有清晰的答案了。

const addFunc = WrappedComponent => class extends Component {
  handleClick() {
    console.log('click');
  }
  
  render() {
    const props = {
      ...this.props,
      handleClick: this.handleClick,
    };
    return <WrappedComponent {...props} />;
  }
};

const addStyle = WrappedComponent => class extends Component {

  render() {
    return (<div style={{ color: '#76d0a3' }}>
      <WrappedComponent {...this.props} />
    </div>);
  }
};

const WrappenComponent = addStyle(addFunc(Usual));

class WrappedUsual extends Component {

  render() {
    console.log(this.props, 'props');
    return (<div>
      <WrappedComponent />
    </div>);
  }
}

显然 HOC 是更优雅一些的,每个HOC都定义自己独有的处理逻辑,需要的时候只需要去包装你的组件。相较于 Container 的方式,HOC耦合性更低,灵活性更高,可以自由组合,更适合应付复杂的业务。当然当你的需求很简单的时候,还是用Container去自由组合,应用场景需要你清楚。

注意点(约束)

其实官网有很多,简单介绍一下。

最重要的原则就是,注意高阶组件不会修改子组件,也不拷贝子组件的行为。高阶组件只是通过组合的方式将子组件包装在容器组件中,是一个无副作用的纯函数

要给 hoc 添加 class 名,便于 debugger。我上面的好多栗子组件都没写class 名,请不要学我,因为我实在想不出叫什么名了... 当我们在chrome里应用React-Developer-Tools的时候,组件结构可以一目了然,所以 DisplayName最好还是加上。

constructor

静态方法要复制
无论 PP 还是II的方式,WrappedComponent 的静态方法都不会复制,如果要用需要我们单独复制。

refs 不会传递。 意思就是 HOC 里指定的 ref,并不会传递到子组件,如果你要使用最好写回调函数通过props传下去。

不要在 render 方法内部使用高阶组件。简单来说 react 的差分算法会去比较 NowElement === OldElement,来决定要不要替换这个elementTree。也就是如果你每次返回的结果都不是一个引用,react 以为发生了变化,去更替这个组件会导致之前组件的状态丢失。

 // HOC不要放到render函数里面
 
 class WrappedUsual extends Component {

  render() {
    const WrappenComponent = addStyle(addFunc(Usual));

    console.log(this.props, 'props');
    return (<div>
      <WrappedComponent />
    </div>);
  }
}

使用 compose 组合 HOC。函数式编程的套路...,例如应用 redux 中的 middleware 以增强功能。redux-middleware 解析

const addFuncHOC = ...
const addStyleHOC = ...//省略

const compose = (...funcs) => component => {
  if (funcs.lenght === 0) {
    return component;
  }
  const last = funcs[funcs.length - 1];
  return funcs.reduceRight((res, cur) => cur(res), last(component));
};

const WrappedComponent = compose(addFuncHOC, addStyleHOC)(Usual);

关于注意点,官网有所介绍,不再赘述。链接

总结

高阶组件最大的好处就是解耦和灵活性,在 react 的开发中还是很有用的。当然这不可能是高阶组件的全部用法。掌握了它的一些技巧,还有一些限制,你可以结合你的应用场景,发散思维,尝试一些不同的用法。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(18

愿与i 2022-05-04 13:46:26

@sunyongjian 对啊,而且这边最近刚刚转react native,现在要是换框架,感觉要雪崩了。。。目前还没有可以代替react native的框架。。。

奈何桥上唱咆哮 2022-05-04 13:46:26

这篇文章太棒了。

两相知 2022-05-04 13:46:26

return class extends Component 这句 怎解释?返回的是一个组件 还是类?

神爱温柔 2022-05-04 13:46:26

@stephenzhao Class 啊,组件的类,使用的时候实例化

倚栏听风 2022-05-04 13:46:26

超赞 写的很好 感谢

小嗷兮 2022-05-04 13:46:26

@sunyongjian 对啊,而且这边最近刚刚转react native,现在要是换框架,感觉要雪崩了。。。目前还没有可以代替react native的框架。。。
用flutter啊

治碍゛ 2022-05-04 13:46:26

老哥,

不如归去 2022-05-04 13:46:18

@sunyongjian 我们公司的技术栈目前是上古老业务还是Lizard(基于backbone的一个Hybrid框架)的,新业务基本都是react了,只不过还是受制于Lizard框架,react只能在单独的页面中用。。。
刚刚老大发消息说,因为最近facebook那个协议风波,上面的大领导正在讨论要不要换框架。。。

∞白馒头 2022-05-04 13:46:18

@yinguangyao 哦哦。react 的多页面呗。
协议早就有了... 只不过才被挖出来吧。不过这是法律方面的东西问题了... 咱们也不懂

假装不在乎 2022-05-04 13:46:15

嗯,对。这些东西肯定之前是不可能想到的,所以你组件要拆的够细呀。你现在用 React 吗 @yinguangyao

会傲 2022-05-04 13:46:13

@sunyongjian 因为公司的业务问题吧,也没有太多机会去实践这个,不过我理解你的意思了,在有些需要复用的场景,比如添加一个样式、页面加载的loading这种,关于你说的checkbox这个例子,假如有场景,最多只让选中两个,但是后来改需求了,最多选中三个,或者在其他页面也有checkbox,但是让最多选四个,这个时候应该是适合高阶函数的,只是有时候一开始意识不到这个组件以后会不会被复用

那片花海 2022-05-04 13:44:51

@yinguangyao 我个人觉得,高阶组件跟你写不写无状态组件是木有关系的,不是说无状态组件的复用性就一定好。首先介绍我理解的无状态组件的使用场景。并不是任何时候,无状态组件都是最佳选择。比如有一些状态维护到内部,要比 container 组件维护要好很多,举个例子,CheckBoxGroup 这种,选中的状态,打钩不打钩,我觉得 CheckBox 自己有个 state 去控制就好了。如果 CheckBox 是无状态的,父组件可能就需要一个数组去维护每一个的选中状态,我觉得是更麻烦一些的。还是看使用场景吧,无状态组件大多数情况下还是比较好的。
然后就是高阶组件,能用 container 就不需要引入高阶组件啊,只是我们意识到有时候高阶组件的复用性,效果更好的时候,才会用。比如做个 loading?某些权限控制? 这些可以被抽进来的逻辑,尽管按类别封进高阶组件,在不同的页面去使用。
所以,并不一定全部都是无状态组件+高阶函数就是最好的。高阶组件只是更优雅的去处理 container 组件不好处理的一些情况。
复用性是牵扯到很多方面的,理解业务,你想好怎么拆分组件,拓展性,组件的写法等等。不知道能不能回答你的问题

メ斷腸人バ 2022-05-04 13:42:28

最近也看了不少关于高阶组件的文章,在开发中,是不是一开始就全部用无状态组件+高阶函数的形式最好呢?把状态之类的剥离出来都写到高阶函数里面,这样无状态组件也可以很容易就被复用了

夜吻♂芭芘 2022-05-04 13:40:14

@nsuedu 3q... 好评。
注意点没写例子… 可能当时偷懒了吧。

晚风撩人 2022-05-04 13:36:36

不过 注意点(约束)里面怎么突然没有栗子了呢

隔纱相望 2022-05-04 13:20:00

这么好的文章哪里去找

吻安 2022-05-04 05:12:36

很赞

青衫负雪 2022-05-03 16:51:06

great!

~没有更多了~

关于作者

娇纵

暂无简介

0 文章
0 评论
24 人气
更多

推荐作者

遂心如意

文章 0 评论 0

5513090242

文章 0 评论 0

巷雨优美回忆

文章 0 评论 0

junpengz2000

文章 0 评论 0

13郎

文章 0 评论 0

qq_xU4RDg

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文