https://www.manster.me/?p=616
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、属性代理
属性代理是最常见的高阶组件的实现方式:
1
2
3
4
5
6
7
8
|
function
MyContainer
(
WrappedComponent
)
{
return
class
Box
extends
React
.
Component
{
render
(
)
{
.
.
.
.
return
<
WrappedComponent
{
.
.
.
this
.
props
}
/
>
}
}
}
|
最重要的部分是render中返回来传入的WrappedComponent 的React组件。这样我们可以通过高阶组件来传递、修改props,这种方法即为属性代理。
当我们使用MyContainer这个高阶组件就会变得很容易:
1
2
3
4
5
|
class
MyComponent
extends
React
.
Component
{
.
.
.
.
}
export
default
MyContainer
(
MyComponent
)
;
|
这样组件就可以一层层的作为参数被调用,原始组价就具备了高阶组件对它的修饰。保持了单个组件封装性的同时还保留了易用性。当然我们也可以通过decorator
来转换:
decorator是ES7的新特性,这里不多说,有兴趣可以看一下这里
1
2
3
4
5
6
|
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作新的命名以防止混淆
123456789101112131415161718192021222324class 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 的实例的引用。123456789101112131415161718192021222324class 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组件来说明
1234567891011121314151617181920212223242526function 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 } / >}}}
可以这样使用
123456@ ppHOCclass Example extends React . Component {render ( ) {return < input name = "name" { . . . this . props . name } / >}}
这样我们就得到一个受控input组件。 - 使用其他元素包裹wrappedComponent这么做是为了布局或者样式处理等目的,我们可以在wrappedComponent 外包裹其他元素或者组件
1234567891011function ppHOC ( WrappedComponent ) {return class PP extends React . Component {render ( ) {return (< div style = { { display : 'block' } } >< WrappedComponent { . . . this . props } / >< / div >)}}}2、反向继承
另一种实现高阶组件的方式成为反向继承。先看一个简单的实现:
1234567function 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)
1234567891011function MyContainer ( WrappedComponent ) {return class MyComponent extends WrappedComponent {render ( ) {if ( this . props . loggedIn ) {return super . render ( )} else {return null}}}}例2:修改由 render 方法输出的 React 组件树。
1234567891011121314function 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 混在一起。
1234567891011121314export 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:
12345678HOC . displayName = ` HOC ( $ { getDisplayName ( WrappedComponent ) } ) `//或class HOC extends . . . {static displayName = ` HOC ( $ { getDisplayName ( WrappedComponent ) } ) `. . .}getDisplayName 函数:
12345function getDisplayName ( WrappedComponent ) {return WrappedComponent . displayName ||WrappedComponent . name ||‘ Component’}我们可以使用recompose库,它已经帮我们实现了相应的方法。
4、组件参数
有的时候我们调用高级组件的时候组要传入一些参数。我们可以这么做:
例子:Props Proxy 模式 的 HOC 最简参数使用方法。关键在于 HOCFactoryFactory 函数。
12345678910function HOCFactoryFactory ( . . . params ) {// do something with paramsreturn function HOCFactory ( WrappedComponent ) {return class HOC extends React . Component {render ( ) {return < WrappedComponent { . . . this . props } / >}}}}你可以这样用:
1234HOCFactoryFactory ( params ) ( WrappedComponent )//或@ HOCFatoryFactory ( params )class WrappedComponent extends React . Component { }这是利用了函数式编程了特性。
参考阅读:https://zhuanlan.zhihu.com/p/24776678#!
书籍:《深入React技术栈》
- 渲染劫持(Render Highjacking)
- 抽象state