higher-order 这个词业界老司机基本上都知道,higher-order-function(高阶函数)在函数式编程中是一种概念。描述的是一种函数:这种函数接受函数作为参数,或者输出一个参数。 常见的有map
、sort
、reduce
higher-order-component (高阶组件HOC)类似于高阶函数,它接受一个React组件作为参数,返回一个新的React组件。
通俗点讲就是:当React组件呗包裹时(wrapped),高阶组件会返回一个加强的React组件。 高阶组件让我们的代码更具有复用性、逻辑性和抽象特征。它可以对render方法作劫持,也可以控制props和state
实现高阶组件有两种方式:
- 属性代理(props proxy)。高阶组件通过被包裹的React组件来操作props
- 反向继承(inheritance inversion)。 高阶组件继承于被包裹的React组件。
1、属性代理
属性代理是最常见的高阶组件的实现方式:
function MyContainer(WrappedComponent) { return class Box extends React.Component { render() { .... return <WrappedComponent {...this.props}/> } } }
最重要的部分是render中返回来传入的WrappedComponent 的React组件。这样我们可以通过高阶组件来传递、修改props,这种方法即为属性代理。
当我们使用MyContainer这个高阶组件就会变得很容易:
class MyComponent extends React.Component{ .... } export default MyContainer(MyComponent);
这样组件就可以一层层的作为参数被调用,原始组价就具备了高阶组件对它的修饰。保持了单个组件封装性的同时还保留了易用性。当然我们也可以通过decorator
来转换:
decorator是ES7的新特性,这里不多说,有兴趣可以看一下这里
import React,{Component} from 'React'; @MyContainer class MyComponent extends Component{ render (){} } export default MyComponent;
简单的替换为作用在类上的decorator,既接受需要装饰的类为参数,返回一个新的内部类。这与高阶组件的定义完全一致。因此,可以认为作用在类上的decorator语法糖简化了高阶组件的调用。
从功能上来讲,高阶组件能做到控制props、通过refs使用引用、抽象state、使用其他元素包裹wrappedComponent。
- 控制props
你可以读取、添加、编辑、删除传给 WrappedComponent 的 props。但是要小心操作重要的props,尽可能对高阶组件的props作新的命名以防止混淆class InBox extends React.Component { render() { return ( <div>this.is {this.props.a}+{this.props.b}</div> ) } } function HOC(InBox) { return class box extends React.Component { render() { let props = { ...this.props, a: 'aaa', b: 'bbb', } return ( <InBox {...props}/> ) } } } export default HOC(InBox) //->render 结果: this.is aaa+bbb
- 通过refs使用引用
在高阶组件中,我们可以接受refs使用WrappedComponent的组件。
你可以通过引用(ref)访问到 this (WrappedComponent 的实例),但为了得到引用,WrappedComponent 还需要一个初始渲染,意味着你需要在 HOC 的 render 方法中返回 WrappedComponent 元素,让 React 开始它的一致化处理,你就可以得到 WrappedComponent 的实例的引用。
class In extends Component { handleClick(){ console.log(this) } render() { return ( <button onClick={::this.handleClick}>click</button> ) } } function refsHOC(WrappedComponent) { return class RefsHOC extends React.Component { proc(wrappedComponentInstance) { console.log(wrappedComponentInstance) // wrappedComponentInstance.method() } render() { const props = Object.assign({}, this.props, {ref: this.proc.bind(this)}) return <WrappedComponent {...props}/> } } } export default refsHOC(In)
Ref 的回调函数会在 WrappedComponent 渲染时执行,你就可以得到 WrappedComponent 的引用。这可以用来读取/添加实例的 props ,调用实例的方法。
- 抽象state
我们可以通过传入 props 和回调函数把 state 提取出来
抽象一个input组件来说明function ppHOC(WrappedComponent) { return class PP extends React.Component { constructor(props) { super(props) this.state = { name: '' } this.onNameChange = this.onNameChange.bind(this) } onNameChange(event) { this.setState({ name: event.target.value }) } render() { const newProps = { name: { value: this.state.name, onChange: this.onNameChange } } return <WrappedComponent {...this.props} {...newProps}/> } } }
可以这样使用
@ppHOC class Example extends React.Component { render() { return <input name="name" {...this.props.name}/> } }
这样我们就得到一个受控input组件。
- 使用其他元素包裹wrappedComponent这么做是为了布局或者样式处理等目的,我们可以在wrappedComponent 外包裹其他元素或者组件
function ppHOC(WrappedComponent) { return class PP extends React.Component { render() { return ( <div style={{display: 'block'}}> <WrappedComponent {...this.props}/> </div> ) } } }
2、反向继承
另一种实现高阶组件的方式成为反向继承。先看一个简单的实现:
function MyContainer(WrappedComponent) { return class MyComponent extends WrappedComponent { render() { return super.render() } } }
高阶组件返回的组件继承于wrappedComponent,因为被动的继承了WrappedComponent,所以所有的调用都会反向。这也是这种方法的由来。
在反向继承中高阶组价可以通过this来访问到wrappedComponent,意味着它可以访问到 state、props、组件生命周期方法和 render 方法。但是它不能保证完整的子组件树被解析。
反向继承可以做什么?
- 渲染劫持(Render Highjacking)
之所以被称为渲染劫持是因为 HOC 控制着 WrappedComponent 的渲染输出,可以用它做各种各样的事。
通过渲染劫持你可以:- 在由 render输出的任何 React 元素中读取、添加、编辑、删除 props
- 读取和修改由 render 输出的 React 元素树
- 有条件地渲染元素树
- 把样式包裹进元素树(就像在 Props Proxy 中的那样)
就像刚才说的,反向继承不能保证完整的子组件呗解析,这以为这将限制渲染劫持的功能。使用渲染劫持你可以完全操作 WrappedComponent 的 render 方法返回的元素树。但是如果元素树包括一个函数类型的 React 组件,你就不能操作它的子组件了。(被 React 的一致化处理推迟到了真正渲染到屏幕时)
例1:条件渲染。当 this.props.loggedIn 为 true 时,这个 HOC 会完全渲染 WrappedComponent 的渲染结果。(假设 HOC 接收到了 loggedIn 这个 prop)
function MyContainer(WrappedComponent) { return class MyComponent extends WrappedComponent { render() { if (this.props.loggedIn) { return super.render() } else { return null } } } }
例2:修改由 render 方法输出的 React 组件树。
function MyContainer(WrappedComponent) { return class MyComponent extends WrappedComponent { render() { const elementsTree = super.render() let newProps = {}; if (elementsTree && elementsTree.type === 'input') { newProps = {value: 'may the force be with you'} } const props = Object.assign({}, elementsTree.props, newProps) const newElementsTree = React.cloneElement(elementsTree, props, elementsTree.props.children) return newElementsTree } } }
在这个例子中,如果 WrappedComponent 的输出在最顶层有一个 input,那么就把它的 value 设为 “may the force be with you”。
你可以在这里做各种各样的事,你可以遍历整个元素树,然后修改元素树中任何元素的 props。这也正是样式处理库 Radium 所用的方法
- 操作 state
HOC 可以读取、编辑和删除 WrappedComponent 实例的 state,如果你需要,你也可以给它添加更多的 state。记住,这会搞乱 WrappedComponent 的 state,导致你可能会破坏某些东西。要限制 HOC 读取或添加 state,添加 state 时应该放在单独的命名空间里,而不是和 WrappedComponent 的 state 混在一起。
export function MyContainer(WrappedComponent) { return class MyComponent 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 实例的 props 和 state
3、组件命名
当包裹了一个高阶组件时,我们失去了原始的wrappedComponent 的displayName,而组件名字方便我们开发和调试的重要属性。
通常会用 WrappedComponent 的名字加上一些 前缀作为 HOC 的名字。下面的代码来自 React-Redux:
HOC.displayName = `HOC({getDisplayName(WrappedComponent)})`
//或
class HOC extends ... {
static displayName = `HOC({getDisplayName(WrappedComponent)})`
...
}
getDisplayName 函数:
function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || ‘Component’ }
我们可以使用recompose库,它已经帮我们实现了相应的方法。
4、组件参数
有的时候我们调用高级组件的时候组要传入一些参数。我们可以这么做:
例子:Props Proxy 模式 的 HOC 最简参数使用方法。关键在于 HOCFactoryFactory 函数。
function HOCFactoryFactory(...params){ // do something with params return function HOCFactory(WrappedComponent) { return class HOC extends React.Component { render() { return <WrappedComponent {...this.props}/> } } } }
你可以这样用:
HOCFactoryFactory(params)(WrappedComponent) //或 @HOCFatoryFactory(params) class WrappedComponent extends React.Component{}
这是利用了函数式编程了特性。
参考阅读:https://zhuanlan.zhihu.com/p/24776678#!
书籍:《深入React技术栈》
文章评论