如何理解 React 高阶组件(HOC)?

3,708 阅读5分钟

本篇文章适合想要了解 React 高阶组件(React High-Order Component) 而且对函数式编程不熟悉的同学阅读,不合适对高阶组件以及函数式编程很熟悉的同学阅读(写的有点啰嗦哈哈)。本篇文章的目的是让没有函数式编程经验的同学理解高阶组件,一瞥高阶组件的强大,希望大家可以把高阶组件用到自己的项目中去。

好了废话说完了,让我们进入正题。在说高阶组件是啥之前,我们先来看看 React Component 是啥:

const Person = props => <div>{props.name}</div>

这里的 Person 组件接收 props 返回 React element,习惯上把这种组件叫做 Functional Component,但你可以把 Person 理解是一个函数,它接收的参数是 props,返回的是 React Element。

下面我们来看看高阶组件是啥:

const identity = func => func

这里的 identity 接受一个 function,然后原样返回这个 function,我说这个 identity 函数是个高阶函数大家应该没有疑问吧?对,identity就是一个简单的高阶函数。如果我们把组件理解成函数,高阶组件就可以理解为是高阶函数。高阶函数接收函数作为函数的参数,返回一个函数;高阶组件接受组件作为函数的参数,返回一个组件。 现在我们把 identity 的形式换一下:

const identity = BaseComponent => BaseComponent

这里的 identity 就是一个简单的高阶组件,它接受一个组件,然后原样返回这个组件。说白了,我觉得高阶组件和高阶函数本质上就是一毛一样的东西。熟悉 ramdajs 的同学,对 identity 函数一定不陌生,不过我们这里的 identity 作为高阶组件,用处不是很大。

有同学可能会问了这就是高阶组件嘛?有啥用?我们下面来看一个贴近实际的例子:

const List = ({ data }) => (
  <ul>
    {data.map(item => <li key={item.name}>{item.name}</li>)}
  </ul>
)

我们有一个这样的 List 组件,如果我们想给这个 List 组件加一个 loading 功能,我们可以这么做:

const List = ({ data, isLoading }) => (
  isLoading ?
    <div>我正在加载...</div> :
    <ul>
      {data.map(item => <li key={item.name}>{item.name}</li>)}
    </ul>
)

我们修改一下 List 组件的代码,让它根据 isLoading 的状态来判断是否出现加载动画。这样做不是不可以,但是不够优雅。第一,这么做需要修改原来组件的代码;第二,如果我们有其它组件,比如 Table也需要 loading 功能,我们又需要重复写相同的判断逻辑。我们用高阶组件/函数就可以完美的解决上面两个问题。

const withLoading = BaseComponent => ({ isLoading, ...otherProps }) => (
  isLoading ?
    <div>我正在加载...</div> :
    <BaseComponent {...otherProps} />
)

const LoadingList = withLoading(List)

我们这里的 withLoading 接受一个 BaseComponent,然后返回一个加强了的组件(LoadingList),我们在 LoadingList 内部处理了 loading 的判断逻辑,这样我们既不用修改 List 的码,也可以复用loading 的判断逻辑。以后有组件需要 loading 功能我就就可以通过调用 withLoading 来实现,你也可以用 decorator (@withLoading) 的形式来调用。

@withLoading
class Table extends React.Component {
  //...
}

withLoading 的使用方式和 react-router 里的 withRouter 使用方式很像对吧,withRouter 就是一个高阶组件,会把路由相关的一些数据注入到传进来的组件中去。

我们再看一个的例子:

const flattern = propKey => BaseComponent => props => <BaseComponent {...props} {...props[propKey]} />

我们的这个 flatten 函数接受了一个 propKey 然后返回了一个高阶组件,它的作用就是将 props 里的某个 key 扁平化(调用形式有点像redux 里的 connect 对不对),这样做有什么好处?

const FlatternLoadingList = flattern('list')(LoadingList)
...
// 我们就可以这样调用组件
<FlatternLoadingList list={...}/>
// 而不用像下面这样一个个手动传递 props
<LoadingList data={...} isLoading={...}/>

假设我们 List 组件需要读取 router 里的参数了,整个调用过程就会变成这样:

const LoadingList = withLoading(List)

const FlatternLoadingList = flattern('list')(LoadingList)

const EnhancedList = withRouter(FlatternLoadingList)

// 或者是下面这样, 难以阅读
const EnhancedList2 = withRouter(flattern('list')(withLoading(List)))

如果我们需要调用的高阶组件越来越多,调用过程会变得繁琐、难以阅读,这是时候我们需要引入 compose。compose 在函数式编程里的应用非常普遍, redux 里也提供了 compose 函数。关于 compose 的延伸阅读可以参考这里。利用 compose 我们就写成这样:

const EnhancedList = compose(
  withRouter,
  flattern('list'),
  withLoading,
)(List)

是不是很简洁?我们也可以用 pipe 函数,它和 compose 函数类似, 只不过它的组合顺序是从左到右而 compose 是从右到左。

Higher-order Components 这个 pattern 最早是sebmarkbage 在 gist 上提出来的,现在 React 社区里可以看到大量高阶组件以及函数式编程的影子。关于高阶组件的一些高级用法、性能问题这里就不做讨论了, 感兴趣的同学自己查找其它资料。

最后希望本文对大家有所帮助,建议大家系统的学习一下函数式编程。函数式编程刚开始学起来可能觉得很奇怪,很难看懂,坚持下去,你会爱上它哈哈!

参考链接

Higher-Order Components - React

medium.com/@franleplan…

www.youtube.com/watch?v=BcV…

github.com/MostlyAdequ…

www.youtube.com/watch?v=zD_…