摘要:在项目中用好高阶组件,可以显著提高代码质量。高阶组件的定义类比于高阶函数的定义。高阶函数接收函数作为参数,并且返回值也是一个函数。
React 深入系列,深入讲解了React中的重点概念、特性和模式等,旨在帮助大家加深对React的理解,以及在项目中更加灵活地使用React。1. 基本概念
高阶组件是React 中一个很重要且比较复杂的概念,高阶组件在很多第三方库(如Redux)中都被经常使用。在项目中用好高阶组件,可以显著提高代码质量。
高阶组件的定义类比于高阶函数的定义。高阶函数接收函数作为参数,并且返回值也是一个函数。类似的,高阶组件接收React组件作为参数,并且返回一个新的React组件。高阶组件本质上也是一个函数,并不是一个组件,这一点一定不要弄错。
2. 应用场景为什么React引入高阶组件的概念?它到底有何威力?让我们先通过一个简单的例子说明一下。
假设有一个组件MyComponent,需要从LocalStorage中获取数据,然后渲染数据到界面。我们可以这样写组件代码:
import React, { Component } from "react" class MyComponent extends Component { componentWillMount() { let data = localStorage.getItem("data"); this.setState({data}); } render() { return{this.state.data}} }
代码很简单,但当有其他组件也需要从LocalStorage中获取同样的数据展示出来时,需要在每个组件都重复componentWillMount中的代码,这显然是很冗余的。下面让我们来看看使用高阶组件可以怎么改写这部分代码。
import React, { Component } from "react" function withPersistentData(WrappedComponent) { return class extends Component { componentWillMount() { let data = localStorage.getItem("data"); this.setState({data}); } render() { // 通过{...this.props} 把传递给当前组件的属性继续传递给被包装的组件WrappedComponent return} } } class MyComponent2 extends Component { render() { return {this.props.data}} } const MyComponentWithPersistentData = withPersistentData(MyComponent2)
withPersistentData就是一个高阶组件,它返回一个新的组件,在新组件的componentWillMount中统一处理从LocalStorage中获取数据的逻辑,然后将获取到的数据以属性的方式传递给被包装的组件WrappedComponent,这样在WrappedComponent中就可以直接使用this.props.data获取需要展示的数据了,如MyComponent2所示。当有其他的组件也需要这段逻辑时,继续使用withPersistentData这个高阶组件包装这些组件就可以了。
通过这个例子,可以看出高阶组件的主要功能是封装并分离组件的通用逻辑,让通用逻辑在组件间更好地被复用。高阶组件的这种实现方式,本质上是一个装饰者设计模式。
高阶组件的参数并非只能是一个组件,它还可以接收其他参数。例如,组件MyComponent3需要从LocalStorage中获取key等于name的数据,而不是上面例子中写死的key等于data的数据,withPersistentData这个高阶组件就不满足我们的需求了。我们可以让它接收额外的一个参数,来决定从LocalStorage中获取哪个数据:
import React, { Component } from "react" function withPersistentData(WrappedComponent, key) { return class extends Component { componentWillMount() { let data = localStorage.getItem(key); this.setState({data}); } render() { // 通过{...this.props} 把传递给当前组件的属性继续传递给被包装的组件WrappedComponent return} } } class MyComponent2 extends Component { render() { return {this.props.data}} //省略其他逻辑... } class MyComponent3 extends Component { render() { return{this.props.data}} //省略其他逻辑... } const MyComponent2WithPersistentData = withPersistentData(MyComponent2, "data"); const MyComponent3WithPersistentData = withPersistentData(MyComponent3, "name");
新版本的withPersistentData就满足我们获取不同key的值的需求了。高阶组件中的参数当然也可以是函数,我们将在下一节进一步说明。
3. 进阶用法高阶组件最常见的函数签名形式是这样的:
HOC([param])([WrappedComponent])
用这种形式改写withPersistentData,如下:
import React, { Component } from "react" const withPersistentData = (key) => (WrappedComponent) => { return class extends Component { componentWillMount() { let data = localStorage.getItem(key); this.setState({data}); } render() { // 通过{...this.props} 把传递给当前组件的属性继续传递给被包装的组件WrappedComponent return} } } class MyComponent2 extends Component { render() { return {this.props.data}} //省略其他逻辑... } class MyComponent3 extends Component { render() { return{this.props.data}} //省略其他逻辑... } const MyComponent2WithPersistentData = withPersistentData("data")(MyComponent2); const MyComponent3WithPersistentData = withPersistentData("name")(MyComponent3);
实际上,此时的withPersistentData和我们最初对高阶组件的定义已经不同。它已经变成了一个高阶函数,但这个高阶函数的返回值是一个高阶组件。HOC([param])([WrappedComponent])这种形式中,HOC([param])才是真正的高阶组件,我们可以把它看成高阶组件的变种形式。这种形式的高阶组件因其特有的便利性——结构清晰(普通参数和被包裹组件分离)、易于组合,大量出现在第三方库中。如react-redux中的connect就是一个典型。connect的定义如下:
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])(WrappedComponent)
这个函数会将一个React组件连接到Redux 的 store。在连接的过程中,connect通过函数类型的参数mapStateToProps,从全局store中取出当前组件需要的state,并把state转化成当前组件的props;同时通过函数类型的参数mapDispatchToProps,把当前组件用到的Redux的action creators,以props的方式传递给当前组件。
例如,我们把组件ComponentA连接到Redux上的写法类似于:
const ConnectedComponentA = connect(mapStateToProps, mapDispatchToProps)(ComponentA);
我们可以把它拆分来看:
// connect 是一个函数,返回值enhance也是一个函数 const enhance = connect(mapStateToProps, mapDispatchToProps); // enhance是一个高阶组件 const ConnectedComponentA = enhance(ComponentA);
当多个函数的输出和它的输入类型相同时,这些函数是很容易组合到一起使用的。例如,有f,g,h三个高阶组件,都只接受一个组件作为参数,于是我们可以很方便的嵌套使用它们:f( g( h(WrappedComponent) ) )。这里可以有一个例外,即最内层的高阶组件h可以有多个参数,但其他高阶组件必须只能接收一个参数,只有这样才能保证内层的函数返回值和外层的函数参数数量一致(都只有1个)。
例如我们将connect和另一个打印日志的高阶组件withLog联合使用:
const ConnectedComponentA = connect(mapStateToProps)(withLog(ComponentA));
这里我们定义一个工具函数:compose(...functions),调用compose(f, g, h) 等价于 (...args) => f(g(h(...args)))。用compose函数我们可以把高阶组件嵌套的写法打平:
const enhance = compose( connect(mapStateToProps), withLog ); const ConnectedComponentA = enhance(ComponentA);
像Redux等很多第三方库都提供了compose的实现,compose结合高阶组件使用,可以显著提高代码的可读性和逻辑的清晰度。
4.与父组件区别有些同学可能会觉得高阶组件有些类似父组件的使用。例如,我们完全可以把高阶组件中的逻辑放到一个父组件中去执行,执行完成的结果再传递给子组件。从逻辑的执行流程上来看,高阶组件确实和父组件比较相像,但是高阶组件强调的是逻辑的抽象。高阶组件是一个函数,函数关注的是逻辑;父组件是一个组件,组件主要关注的是UI/DOM。如果逻辑是与DOM直接相关的,那么这部分逻辑适合放到父组件中实现;如果逻辑是与DOM不直接相关的,那么这部分逻辑适合使用高阶组件抽象,如数据校验、请求发送等。
5. 注意事项1)不要在组件的render方法中使用高阶组件,尽量也不要在组件的其他生命周期方法中使用高阶组件。因为高阶组件每次都会返回一个新的组件,在render中使用会导致每次渲染出来的组件都不相等(===),于是每次render,组件都会卸载(unmount),然后重新挂载(mount),既影响了效率,又丢失了组件及其子组件的状态。高阶组件最适合使用的地方是在组件定义的外部,这样就不会受到组件生命周期的影响了。
2)如果需要使用被包装组件的静态方法,那么必须手动拷贝这些静态方法。因为高阶组件返回的新组件,是不包含被包装组件的静态方法。hoist-non-react-statics可以帮助我们方便的拷贝组件所有的自定义静态方法。有兴趣的同学可以自行了解。
3)Refs不会被传递给被包装组件。尽管在定义高阶组件时,我们会把所有的属性都传递给被包装组件,但是ref并不会传递给被包装组件。如果你在高阶组件的返回组件中定义了ref,那么它指向的是这个返回的新组件,而不是内部被包装的组件。如果你希望获取被包装组件的引用,你可以把ref的回调函数定义成一个普通属性(给它一个ref以外的名字)。下面的例子就用inputRef这个属性名代替了常规的ref命名:
function FocusInput({ inputRef, ...rest }) { return ; } //enhance 是一个高阶组件 const EnhanceInput = enhance(FocusInput); // 在一个组件的render方法中... return (下篇预告:{ this.input = input } }>) // 让FocusInput自动获取焦点 this.input.focus();
React 深入系列7:React 常用模式
我的新书《React进阶之路》已上市,请大家多多支持!
链接:京东 当当
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/95196.html
摘要:本篇是深入系列的最后一篇,将介绍开发应用时,经常用到的模式,这些模式并非都有官方名称,所以有些模式的命名并不一定准确,请读者主要关注模式的内容。 React 深入系列,深入讲解了React中的重点概念、特性和模式等,旨在帮助大家加深对React的理解,以及在项目中更加灵活地使用React。 本篇是React深入系列的最后一篇,将介绍开发React应用时,经常用到的模式,这些模式并非都有...
摘要:为了代码进一步解耦,可以考虑使用高阶组件这种模式。开源的高阶组件使用提供了一系列使用的高阶组件,可以增强组件的行为,可以利用此库学习高阶组件的写法。通过使用此库提供的高阶组件,可以方便地让列表元素可拖动。 1. Decorator基本知识 在很多框架和库中看到它的身影,尤其是React和Redux,还有mobx中,那什么是装饰器呢。 修饰器(Decorator)是一个函数,用来修改类的...
摘要:博客地址背景知识在开始讲述高阶组件前,我们先来回顾高阶函数的定义接收函数作为输入,或者输出另一个函数的一类函数,被称作高阶函数。 博客地址:http://www.luckyjing.com/post... 背景知识 在开始讲述高阶组件前,我们先来回顾高阶函数的定义:接收函数作为输入,或者输出另一个函数的一类函数,被称作高阶函数。对于高阶组件,它描述的便是接受React组件作为输入,输出...
阅读 3951·2021-11-11 10:58
阅读 3319·2021-09-26 09:46
阅读 1912·2019-08-30 15:55
阅读 976·2019-08-30 13:52
阅读 1943·2019-08-29 13:11
阅读 3024·2019-08-29 11:27
阅读 1517·2019-08-26 18:18
阅读 2617·2019-08-23 14:17