乐趣区

关于react.js:浅谈React-高阶组件

前文

5、6 月始终忙着本人的琐事,7 月(7 月 31 也还是 7 月嘛)抽空整顿一下旧的内容,有之前的读者提到想理解下高阶组件这块的知识点,就刚好整顿了一下。

高阶组件 HOC(HigherOrderComponent),听起来像是个一个 React 的高级性能,然而实际上它不属于 React API,而应该归为一个是应用技巧或者说设计模式。首先直击实质:

高阶组件是一个函数,并且是一个“参数为组件,返回值为新组件的”的函数。
更直白一点如下:

Fn(组件) => 有更强性能地新组件

这里的 Fn 就是高阶组件。

组件是 React 中的一个根本单元,通常它承受一些 props 属性,并最终展现为具体的 UI,然而某些场景下传统组件还不足以解决问题。

在此分割线下,咱们能够临时抛开干燥的代码,聊一聊生存中一个常见的场景 – 点奶茶。
在当下生存里,奶茶成为很多人生存中的高兴调剂(毕竟生存曾经这么苦了 -_-),而种类口味也是多种多样的,比如说 根底的分类就有纯茶、奶茶、咖啡、鲜榨果汁等,加料也是形形色色,有芝士,牛乳、干果碎,芋泥等 ….(嗯 我筹备先点一杯奶茶,喝完回来持续写)

好的,我回来了~

那么当初就能够形象出几个根底组件:

  • 纯茶
  • 奶茶
  • 果茶

它们别离都可搭配以下加料:

  • 加芝士
  • 加碧根果碎

对于不同根底茶加料的逻辑行为,是类似的,所以 这两种加料形式就能够设计为高阶组件,这样能够很不便地依据须要生成不同类型地最终奶茶。套用后面地函数表达式也就是:

Fn(根底奶茶) => 不同风味的奶茶

这里的 Fn 也就是加料函数。它的作用是让一款根本的奶茶通过加料变成一款加强的奶茶。

注释

到此,置信大家对高阶函数的作用有个大略的概念了,接下来进入正题(醒醒 干燥的来了)。

从一个常见场景说起

置信前端同学都写过不少后盾零碎,天然免不了某些常见性能,比方 操作日志打印,权限管制 等,以操作日志打印为例,要实现以下需要:在进入某些页面组件时,须要打印出日志并传送至服务器。

class Page1 extends React.Component {componentDidMount() {
    // 用 console.log 来模仿日志打印 实际上这里个别会传到服务器保留
    console.log('进入 page1');}
  
  render(){return <div>page1</div>}
}

class Page2 extends React.Component {componentDidMount() {console.log('进入 page2');}
  
  render(){return <div>page2</div>}
}

察看这 Page1 Page2 两种组件都存在一部分类似的逻辑:在 componentDidMount 阶段,须要console.log 以后的页面名称。

当初把这部分逻辑挪动到一个函数外面:

function withLog (WrappedComponent, pageName) {
   // 这个函数接管一个组件作为参数
   return class extends React.Component {componentDidMount() {
       // 用 console.log 来模仿日志打印 实际上这里个别会传到服务器保留
       console.log(pageName);}
     render (){
       // 此处留神要把 this.props 持续透传下去
       return <WrappedComponent {...this.props} />;
     }
   }
}

此时能够解耦掉打印日志的逻辑和具体组件的关联:

class Page1 extends React.Component {
  // 不用保留打印日志的逻辑了
  render(){return <div>page1</div>}
}

class Page2 extends React.Component {
  // 不用保留打印日志的逻辑了

  render(){return <div>page2</div>}
}

// 应用时
const Page1WithLog = withLog(Page1);
const Page2WithLog = withLog(Page2);

这样,就实现了一个简略的高阶组件!

高阶组件做了什么

从下面的例子能够看到,高阶组件 是将传入的组件,包装在容器组件 (容器组件就是 withLog 函数中 return 的匿名组件) 中,最初返回了一个有加强性能的新组件。这里有个很要害的中央是:

  • 不要批改原先的原先的组件!
  • 不要批改原先的原先的组件!
  • 不要批改原先的原先的组件!

相熟 React 的同学会发现,它处处都贯彻着函数式编程的思维,同样的,高阶组件必须是个纯函数(雷同输出必须返回雷同的后果) 这样能力保障组件的可复用性,

高阶组件的组合应用

后面介绍了独自应用一个高阶组件的状况,那么如果要同时应用多个高阶组件呢?连续后面的例子,咱们再设计一个提供 权限治理性能 的高阶组件:

function withCheckPermission (WrappedComponent){
    return class extends React.Component {async componentWillMount() {const hasPermission = await getCurrentUserPermission();
         this.setState({hasPermission});
       }
       render (){
         // 此处留神要把 this.props 持续透传下去
         return (
           {this.state.hasPermission ? 
            <WrappedComponent {...this.props} /> :
           <div> 您没有权限查看该页面,请分割管理员!</div>}
         )
       }
   }
}

checkPermission 函数会检查用户权限,以判断是否容许用户拜访以后页面,接下来咱们心愿给后面 Page1 组件同时加上权限管制和日志打印的性能:

// 当然能够这样写

// 1. 首先附加 Log 性能
const Page1WithLog = withLog(Page1, 'pageName1');
// 2. 在 1 的根底上附加 CheckPermission 性能
const Page1WithLogAndPermission = withCheckPermission(Page1WithLog);

实际上能够间接用 compose 来实现, 这样在应用多个高阶组件时能够更简洁:
// tips:  compose(f, g, h) 等同于 (...args) => f(g(h(...args)))
import {compose} from 'redux';
const Page1WithLogAndPermission = compose(
  Page1WithLogAndPermission,
  (Component) => withLog(Component, 'pageName1'),
);

在后面提到过,高阶组件不会毁坏被包裹组件自身,因而非常适合灵便应用多个组件,实际上产生的成果很相似于在原有的组件外层再包裹了不同的组件。

应用命名来不便调试

因为高阶组件会在 WrapComponent 外层包裹组件,那么在应用的过程,为了不便调试,就很有必要给每个高阶组件设置 displayName 属性,以后面的withLog 为例:

function withLog (WrappedComponent, pageName) {
   return class extends React.Component {static displayName = `withLog(${getDisplayName(WrappedComponent)})`;
     componentDidMount() {
       // 用 console.log 来模仿日志打印 实际上这里个别会传到服务器保留
       console.log(pageName);}
     render (){
       // 此处留神要把 this.props 持续透传下去
       return <WrappedComponent {...this.props} />;
     }
   }
}

function getDisplayName(WrappedComponent) {return WrappedComponent.displayName || WrappedComponent.name || 'Component';}

这样的话调试过程就能够通过调试器的属性,轻易找到最终代码里的每一层组件。

注意事项

注意事项其实大部分和高阶组件的实现实质无关,文中始终在强调,高阶组件的本质是: 用一个新的组件包裹原有的 WrappedComponent 组件,并在新组件减少一些行为,那么包裹势必也会带来一些留神点。

留神传递 props

传递 props 的意义天然不用多说,除高阶组件自身须要的一些专属props 以外,其余的 props 要持续返回给WrappedComponent,如下:

function withSomeFeature (WrappedComponent){
    return class extends React.Component {
       // 省略性能
       render (){
         // 此处留神 extraProp 示意仅仅是以后这个高阶函数要用的 props 
         const {extraProp, ...passThroughProps} = this.props;
         
         // 要把剩下和本人无关的 props 持续透传下去 
         return<WrappedComponent {...passThroughProps} />
       }
   }
}

不要在 render 中应用高阶组件

具体来说不要这样应用:

class Test extends React.Component {render() {const EnhancedComponent = enhance(MyComponent);
    return <EnhancedComponent />;
  }
}

在下面的代码中,每次执行 render 时, const EnhancedComponent = enhance(MyComponent); 返回的是不同的新组 件(因为组件解析最初理论是一个object,也就是一个援用类型的值,所以每次定义相当于是从新生成一个对象),这样导致的后果是,每次该组件和它的所有子组件状态齐全失落。 所以正确的用法是在组件之外,用高阶组件生成所须要的新组件后间接应用新的组件:

const EnhancedComponent = enhance(MyComponent);

class Test extends React.Component {render() {return <EnhancedComponent />;}
}

拷贝静态方法

同样,这也是包裹带来的问题,假如 WrappedComponent 上有个十分好用的办法,然而 通过高阶组件的加强后,如果不加解决,办法就失落了:

// WrappedComponent 原有一些办法
WrappedComponent.staticMethod = function() {/*...*/}

// 应用 HOC
const EnhancedComponent = enhance(WrappedComponent);
// 加强组件没有 staticMethod
typeof EnhancedComponent.staticMethod === 'undefined' // true

解决方案就是去复制静态方法,常见复制的办法有两种:明确晓得有哪些静态方法要拷贝,而后用 Enhance.staticMethod = WrappedComponent.staticMethod; 一一拷贝;应用 hoist-non-react-statics 主动拷贝:
import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {class Enhance extends React.Component {/*...*/}
  // 外围代码
  hoistNonReactStatic(Enhance, WrappedComponent);
  return Enhance;
}

解决 Refs

对于局部应用了 refsWrapComponent,这是无奈间接像 props 的属性一样去透传的,这个应该应用 React.forwardRef API(React 16.3 中引入)来进行解决。ref 的非凡之处前面在其余文章里做详细描述。

聊一下反向继承

在文章开端,也顺带说一下反向继承,之所以放在最初,是因为这种形式并不是 React 官网推崇的形式,官网文档有这么一句:

请留神,HOC 不会批改传入的组件,也不会应用继承来复制其行为。相同,HOC 通过将组件包装在容器组件中来组成新组件。HOC 是纯函数,没有副作用。

然而看到挺多现有的文章都介绍过这种用法,那就顺便简略介绍下,仅仅作为理解,并不举荐应用(我目前实际里也尚未遇到非用这种不可的场景,如果碰到前面另行补充)。

回顾一下后面提到高阶组件的时候,提到的始终是 装璜者模式 用新组建包裹旧组件做加强 等关键词,与此不同的是,反向代理的思路是这样的,间接上示例代码:

function withHeader (WrappedComponent){
    // 请留神这里是 extends WrappedComponent 而不是 extends React.Component
    return class extends WrappedComponent {render (){
         <div>
            <h1 className='demo-header'></h1>
            // 留神此处要调用父类的 render 函数
            {super.render() }
         </div>
       }
   }
}

察看这个例子的重点局部: 高阶组件实际上是返回了一个继承 WrappedComponent 组件的新组件,这也是反向继承命名的由来。在这种模式下,次要能够有两种操作:

  • 渲染劫持,如上图的例子可见,在返回的新组件里其实能够管制 WrappedComponentrender后果并且执行各种须要的操作,包含选择性的渲染 WrappedComponent 的子树
  • 操作 state,因为在新组件能够通过 this 拜访到 WrappedComponent,所以同样能够通过this.state 来批改它。
    这种实现高阶组件的形式如果真的要应用,肯定要十分审慎,渲染劫持须要思考条件渲染(即不齐全返回子树)的状况,而操作 state 也有可能在某些状况下毁坏父组件的原有逻辑。

审慎应用,审慎应用,审慎应用

总结

水着水着又到结尾了,简略回顾下本文的次要内容:

  • 高阶组件的实质是一个函数,入参和返回值均为组件,作用是给组件加强某个特定的性能
  • 高阶组件举荐灵便组合的应用形式
  • 应用过程中要记得一些注意事项
  • 大略理解反向继承的原理,然而要审慎应用

对于 ref 这块挖了个坑,因为真要写起来还是蛮多内容的,本着每篇文章应该主题清晰,内容简练,让读者 10 分钟内学到常识的准则,决定还是前面独自再写

最初的最初,首先是感激每个关注的读者敌人(尤其是这位催更的读者,有机会请你喝奶茶),欢送大家关注专栏,也心愿大家对于青睐的文章,可能不吝点赞和珍藏,对于行文格调和内容有任何意见的,都欢送私信交换。

退出移动版