概述
高阶组件( higher-order component ,HOC )是 React 中复用组件逻辑的一种进阶技巧,通俗的讲,高阶组件就是一个 React 组件包裹着另外一个 React 组件。它本身并不是 React 的 API,而是一种 React 组件设计理念,众多的 React 库已经证明了它的价值,例如耳熟能详的 react-redux。
高级组件使用函数来实现,基本上是一个类工厂,它的函数签名可以用类似 haskell 的伪代码表示:
hocFactory:: W: React.Component => E: React.Component
其中 W (WrappedComponent) 指被包裹的 React.Component,E (EnhancedComponent) 指返回类型为 React.Component 的新的 HOC。
高阶函数是把函数作为参数传入到函数中并返回一个新的函数。倘若我们把函数替换为组件,就是高阶组件,实现上如下:
const EnhancedComponent = higherOrderComponent(WrappedComponent);
HOC 工厂实现
高阶组件通常有两种实现方式:一种是 Props Proxy,另一种是Inheritance Inversion。
- Props Proxy: HOC 对传给 WrappedComponent W 的 porps 进行操作,并将提取 WrappedComponent state 以及使用其他元素来包裹 WrappedComponent。
- Inheritance Inversion: HOC 继承 WrappedComponent W。
Props Proxy 作为一层代理,会发生隔离,因此传入 WrappedComponent 的 ref 将无法访问到其本身,需在 Props Proxy 内完成中转。
Props Proxy
Props Proxy的简单实现。
function ppHOC(WrappedComponent) {
return class PP extends React.Component
// 实现 HOC 不同的命名
static displayName = `HOC(${WrappedComponent.displayName})`;
getWrappedInstance() {
return this.wrappedInstance;
}
// 实现 ref 的访问
setWrappedInstance(ref) {
this.wrappedInstance = ref;
}
render() {
return <WrappedComponent {
...this.props,
ref: this.setWrappedInstance.bind(this),
} />
}
}
}
HOC 在 render 方法中 返回 了一个 WrappedComponent 类型的 React Element。我们还传入了 HOC 接收到的 props,这就是名字 Props Proxy 的由来。
Props Proxy 的作用
使用 Props Proxy 可以做什么呢?常见的有以下作用:
- 操作 props
- 通过 Refs 访问到组件实例
- 提取 state
- 用其他元素包裹 WrappedComponent
1, 操作 props
你可以读取、添加、编辑、删除传给 WrappedComponent 的 props。当删除或者编辑重要的 props 时要小心,你可能应该通过命名空间确保高阶组件的 props 不会破坏 WrappedComponent。
例如:添加一个新的 props:
function ppHOC(WrappedComponent)
return class PP extends React.Component
render() {
const newProps = {
user: currentLoggedInUser
}
return
2, 通过 Refs 访问到组件实例
你可以通过引用(ref)访问到 this (WrappedComponent 的实例),但为了得到引用,WrappedComponent 还需要一个初始渲染,意味着你需要在 HOC 的 render 方法中返回 WrappedComponent 元素,让 React 开始它的一致化处理,你就可以得到 WrappedComponent 的实例的引用。这个在React 异步开发中会经常用到。
function refsHOC(WrappedComponent)
return class RefsHOC extends React.Component
proc(wrappedComponentInstance) {
wrappedComponentInstance.method()
}
render() {
const props = Object.assign({}, this.props, {ref: this.proc.bind(this)})
return
Ref 的回调函数会在 WrappedComponent 渲染时执行,你就可以得到 WrappedComponent 的引用。
3, 提取 state
你可以通过传入 props 和回调函数把 state 提取出来,类似于 smart component 与 dumb component。
例如,提取了 input 的 value 和 onChange 方法。
@ppHOC
class Example extends React.Component
render() {
return <input name="name" {...this.props.name}/>
}
}
4, 包裹 WrappedComponent
为了封装样式、布局或别的目的,你可以用其它组件和元素包裹 WrappedComponent。
例如,使用包裹样式:
function ppHOC(WrappedComponent)
return class PP extends React.Component
render() {
return (
<div style={{display: 'block'}}>
<WrappedComponent {...this.props}/>
</div>
)
}
}
}
Inheritance Inversion
另一种是 Inheritance Inversion,HOC 类继承了 WrappedComponent,意味着可以访问到 WrappedComponent 的 state、props、生命周期和 render 等方法。如果在 HOC 中定义了与 WrappedComponent 同名方法,将会发生覆盖,就必须手动通过 super 进行调用了。通过完全操作 WrappedComponent 的 render 方法返回的元素树,可以真正实现渲染劫持。这种方案依然是继承的思想,对于 WrappedComponent 也有较强的侵入性,因此并不常见。
例如,下面的代码:
function ppHOC(WrappedComponent) {
return class ExampleEnhance extends WrappedComponent {
...
componentDidMount() {
super.componentDidMount();
}
componentWillUnmount() {
super.componentWillUnmount();
}
render() {
...
return
那么可以还有Inheritance Inversion做什么呢?
- 渲染劫持(Render Highjacking)
- 操作 state
1,渲染劫持
之所以被称为渲染劫持是因为 HOC 控制着 WrappedComponent 的渲染输出,可以用它做各种各样的事。
通过渲染劫持,你可以完成:
- 在由 render输出的任何 React 元素中读取、添加、编辑、删除 props
- 读取和修改由 render 输出的 React 元素树
- 有条件地渲染元素树
- 把样式包裹进元素树,就行Props Proxy那样包裹其他的元素
注:在 Props Proxy 中不能做到渲染劫持。
虽然通过 WrappedComponent.prototype.render 你可以访问到 render 方法,不过还需要模拟 WrappedComponent 的实例和它的 props,还可能亲自处理组件的生命周期,而不是交给 React。记住,React 在内部处理了组件实例,你处理实例的唯一方法是通过 this 或者 refs。
2,操作 state
HOC 可以读取、编辑和删除 WrappedComponent 实例的 state,如果你需要,你也可以给它添加更多的 state。不过这可能会搞坏 WrappedComponent 的 state,从而造成一些莫名其妙的问题。所以,正确的做法是,要限制 HOC 读取或添加 state,添加 state 时应该放在单独的命名空间里,而不是和 WrappedComponent 的 state 混在一起。
例如:通过访问 WrappedComponent 的 props 和 state 来做调试。
export function IIHOCDEBUGGER(WrappedComponent) {
return class II extends WrappedComponent
render() {
return (
<div>
<h2>HOC Debugger Component</h2>
<p>Props</p> <pre>{JSON.stringify(this.props, null, 2)}</pre>
<p>State</p><pre>{JSON.stringify(this.state, null, 2)}</pre>
{super.render()}
</div>
)
}
}
}
用 HOC 包裹了一个组件会使它失去原本 WrappedComponent 的名字,可能会影响开发和调试。通常会用 WrappedComponent 的名字加上一些前缀作为 HOC 的名字。
例如:
HOC.displayName = `HOC(${getDisplayName(WrappedComponent)})`
//或
class HOC extends ... {
static displayName = `HOC(${getDisplayName(WrappedComponent)})`
...
getDisplayName 函数内容:
function getDisplayName(WrappedComponent)
return
相关案例
react-redux
React-Redux 是 Redux 官方的 React 绑定实现。他提供的函数中有一个 connect,处理了监听 store 和后续的处理。是通过 Props Proxy 来实现的。