jtwang7 / React-Note

React 学习笔记
8 stars 2 forks source link

React - 高阶组件 ( HOC ) #30

Open jtwang7 opened 3 years ago

jtwang7 commented 3 years ago

React - 高阶组件 ( HOC )

参考文章:

前言

高阶组件(HOC)是一个函数,它接受一个组件作为参数然后返回一个新组件。 HOC 主要用于组件之间逻辑复用。比如你写了几个组件,他们之间的逻辑几乎相同,就可以用 HOC 对逻辑进行封装复用。

如何封装 HOC

参考 React 官方文档的 HOC 封装案例,主要注意以下几点:

  1. 抽象相同的行为模式,同时向外暴露定制特定行为的参数接口。
    const withXXX = (WrappedComponent, ...params) => {
    // 返回一个组件:类组件 和 函数组件 均可
    return class extends Component {
    // ......
    }
    }
  2. HOC 需要透传 props 给被包裹的组件 HOC 返回的组件与原组件应保持类似的接口。因此 HOC 应该透传与自身无关的 props。

    HOC 封装的公用逻辑,可能会用到部分 props,用于产出新的 state 传递给被包裹组件,而被包裹组件其余的 props 也需要经过 HOC 来传递。因此,被 HOC 包裹所返回的新组件所接受的 props,实际上包含了 HOC 和原组件的 props,HOC 需要将仅用于共用逻辑的 props 过滤,并透传剩余的 props 给被包裹的组件,从而达到原组件包裹前后接口基本保持一致的目的。

render() {
  // 过滤掉非此 HOC 额外的 props
  const { extraProp, ...passThroughProps } = this.props;

  // 逻辑产物
  const injectedProp = someStateOrInstanceMethod;

  // 将公用逻辑产出的 state 与 props 传递给被包装组件
  return (
    <WrappedComponent
      injectedProp={injectedProp}
      {...passThroughProps}
    />
  );
}
  1. 最大化可组合性 上述提到的 HOC 样例中,参数通常接受一个被包裹组件 (WrappedComponent) 以及一系列用于特性化定制的参数 (...params) 形式如下:
    const withXXX = (WrappedComponent, ...params) => { ... }

    但是,更合理的写法,应该将组件和额外参数分开写,如下所示:

    const withXXX = (...params) => (WrappedComponent) => { ... }

    将 withXXX 写成一个返回高阶组件的高阶函数!!! 这种形式可能看起来令人困惑或不必要,但它有一定的设计和理性: 我们可以先传入额外的参数,基于公共逻辑定制并返回一个高阶组件,该高阶组件仅包含单参数 WrappedComponent,因此其形式可表达为:Component => Component,输出类型与输入类型相同的函数很容易组合在一起,从而可实现高阶组件的嵌套组合。

注意事项

不要在 render 方法中使用 HOC

React 的 diff 算法(称为协调)使用组件标识来确定它是应该更新现有子树还是将其丢弃并挂载新子树。 如果从 render 返回的组件与前一个渲染中的组件相同(===),则 React 通过将子树与新子树进行区分来递归更新子树。 如果它们不相等,则完全卸载前一个子树。 若在 render 中使用 HOC,则在每次调用 render 函数都会创建一个新的 EnhancedComponent,这将导致子树每次渲染都会进行卸载,和重新挂载的操作!这不仅仅是性能问题 - 重新挂载组件会导致该组件及其所有子组件的状态丢失。

render() {
  // 每次调用 render 函数都会创建一个新的 EnhancedComponent
  // EnhancedComponent1 !== EnhancedComponent2
  const EnhancedComponent = enhance(MyComponent);
  // 这将导致子树每次渲染都会进行卸载,和重新挂载的操作!
  return <EnhancedComponent />;
}

正确做法应该是在组件之外调用 HOC,并在组件内使用返回的新组件,这样一来组件只会创建一次。因此,每次 render 时都会是同一个组件。一般来说,这跟你的预期表现是一致的。在极少数情况下,你需要动态调用 HOC。你可以在组件的生命周期方法或其构造函数中进行调用。 常见的两种 HOC 调用位置:

  1. 被包裹组件模块导出时:

    // WrappedComponent.js
    class WrappedComponent extends Component {...}
    export default withXXX(WrappedComponent)
  2. 调用组件的外部:

    
    // MyComponent.js
    import WrappedComponent from './WrappedComponent.js';
    import withHOC from './withHOC.js';

const HOCComponent = withHOC( ... )(WrappedComponent);

class MyComponent extends Component { render () { // ...

} }


### 务必复制静态方法
有时在 React 组件上定义静态方法很有用。但是,当你将 HOC 应用于组件时,原始组件将使用容器组件进行包装。这意味着新组件没有原始组件的任何静态方法。
```js
// 定义静态函数
WrappedComponent.staticMethod = function() {/*...*/}
// 现在使用 HOC
const EnhancedComponent = enhance(WrappedComponent);

// 增强组件没有 staticMethod
typeof EnhancedComponent.staticMethod === 'undefined' // true

为了解决这个问题,你可以在返回之前把这些方法拷贝到容器组件上:

function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  // 必须准确知道应该拷贝哪些方法 :(
  Enhance.staticMethod = WrappedComponent.staticMethod;
  return Enhance;
}

但要这样做,你需要知道哪些方法应该被拷贝。你可以使用 hoist-non-react-statics 自动拷贝所有非 React 静态方法:

import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  hoistNonReactStatic(Enhance, WrappedComponent);
  return Enhance;
}

除了导出组件,另一个可行的方案是再额外导出这个静态方法。

// 使用这种方式代替...
MyComponent.someFunction = someFunction;
export default MyComponent;

// ...单独导出该方法...
export { someFunction };

// ...并在要使用的组件中,import 它们
import MyComponent, { someFunction } from './MyComponent.js';

Refs 不会被传递

虽然高阶组件的约定是将所有 props 传递给被包装组件,但这对于 refs 并不适用。那是因为 ref 实际上并不是一个 prop - 就像 key 一样,它是由 React 专门处理的。如果将 ref 添加到 HOC 的返回组件中,则 ref 引用指向容器组件,而不是被包装组件。 这个问题的解决方案是通过使用 React.forwardRef API(React 16.3 中引入)。

jtwang7 commented 3 years ago

React - compose 组合函数实现 HOC 嵌套

参考文章:

前言

组件有时会共用多个类似的逻辑,我们通过拼接多个 HOC 来实现这一目的:

本例中,withData / withLogger / withLoading 为 HOC

const Loading = () => <p>loading</p>;
withData(
"https://jsonplaceholder.typicode.com/posts",
{
_limit: 10,
page: 2
}
)(
withLogger('xxx')(
withLoading(Loading)(Component)
)
)

可以看到,多层的 HOC 嵌套形成的 HOC 嵌套地狱 会导致代码可阅读性很差,因此,我们需要实现一个 compose 组合函数进行优化。

compose

compose 组合函数的本质是 pipe 管道函数。 pipe 管道函数的特点在于:利用 array.reduce() 方法,将上一次的函数结果作为下一次函数的参数进行递归。 而 compose 组合函数与 pipe 区别仅在于组合的方向不同:其利用 array.reduceRight() 方法从右到左对数组进行递归。

// pipe
const pipe = (...fns) => (initArg) => fns.reduce( ( prev, fn ) => fn(prev), initArg );

// compose
const compose = (...fns) => x => fns.reduceRight((x, fn) => fn(x), x);

pipe 和 compose 我们将其写为高阶函数的形式。第一组参数接受一个数组,包含所有要组合的函数,第二组参数接受第一个函数调用的初始参数。执行时利用 reduce 或者 reduceRight 进行递归,其内部回调返回函数的返回值作为下一个函数的参数。

为什么 HOC 需要 reduceRIght 来执行嵌套

从嵌套流角度考虑,我们首先会从最里层开始包裹原始的 WrappedComponent,然后将包裹后返回的新组件传给下一层 HOC 进行封装,因此最终包裹的组件如下:

reduceRight 能够让逻辑书写与实际包裹的形态匹配,更加直观。


// withData( withLogger( withLoading( WrappedComponent ) ) )

const compose = (...fns) => x => fns.reduceRight((x, fn) => fn(x), x); const fns = [withData, withLogger, withLoading ]; const cmp = compose( withData( "https://jsonplaceholder.typicode.com/posts", { _limit: 10, page: 2 } ), withLogger('xxx'), withLoading('loading'), );

cmp(WrappedComponent);


另一方面,我们也可以从这体会到 HOC 写成一个返回高阶组件的高阶函数所带来的优势:在 comopose 中,我们传入的是定制化后的 HOC,最终我们只需要再次调用函数,传入被包裹的组件,就能让 compose 函数内部按照 `Component => Component` 的方式执行,这也就是之前提到的:输入与输出类型相同时,更加容易组合。