原文: https://medium.com/@franleplant/react-higher-order-components-in-depth-cf9032ee6c3ejavascript
本文面向想要探索 HOC 模式的进阶用户,若是你是 React 的初学者则应该从官方文档开始。高阶组件(Higher Order Components)是一种很棒的模式,已被不少 React 库证明是很是有价值的。在本文中,咱们首先回顾一下 HOC 是什么、有什么用、有何局限,以及是如何实现它的。css
在附录中,检视了相关的话题,这些话题并不是 HOC 的核心,但我认为应该说起。html
本文旨在尽可能详细的论述,以便于读者查阅;并假定你已经知晓 ES6。java
走你!react
高阶组件就是包裹了其余 React Component 的组件git
一般,这个模式被实现为一个函数,基本算是个类工厂方法(yes, a class factory!),其函数签名用 haskell 风格的伪代码写出来就是这样的:github
hocFactory:: W: React.Component => E: React.Component
复制代码
W (WrappedComponent) 是被包裹的 React.Component;而函数返回的 E (Enhanced Component) 则是新获得的 HOC,也是个 React.Component。bash
定义中的“包裹”是一种有意的模糊,意味着两件事情:app
后面会详述这两种模式的。函数
在大的维度上 HOC 能用于:
后面将会看到这些类目的细节,但首先来学习一下实现 HOC 的方式,由于实现方式决定了 HOC 实际能作的事情。
属性代理(PP)和继承反转(II)。二者皆提供了不一样的途径以操纵被包裹的组件。
属性代理(Props Proxy)能够用如下方式简单的实现:
function ppHOC(WrappedComponent) {
return class PP extends React.Component {
render() {
return <WrappedComponent {...this.props}/> } } } 复制代码
此处关键的部分在于 HOC 的 render() 方法返回了一个被包裹组件的 React Element。同时,将 HOC 接受到的属性传递给了被包裹的组件,所以称为**“属性代理”**。
注意:
<WrappedComponent {...this.props}/>
// 等价于
React.createElement(WrappedComponent, this.props, null)
复制代码
二者都会建立一个 React Element,用于描述 React 在其一致性比较过程当中应该渲染什么。
了解更多:
关于 React Elment vs Components 的内容能够查看
https://facebook.github.io/react/blog/2015/12/18/react-components-elements-and-instances.html
一致性比较过程
http://www.css88.com/react/docs/reconciliation.html
能够对传递给被包裹组件的属性进行增删查改。但删除或编辑重要属性时要谨慎,应合理设置 HOC 的命名空间以避免影响被包裹组件。
例子:增长新属性。应用中经过 this.props.user 将能够获得已登陆用户
function ppHOC(WrappedComponent) {
return class PP extends React.Component {
render() {
const newProps = {
user: currentLoggedInUser
}
return <WrappedComponent {...this.props} {...newProps}/> } } } 复制代码
能够经过 ref
访问到 this
(被包裹组件的实例),但这须要 ref
所引用的被包裹组件运行一次完整的初始化 render 过程,这就意味着要从 HOC 的 render 方法中返回被包裹组件的元素,并让 React 完成其一致性比较过程,而 ref
能引用该组件的实例就行了。
例子:下例中展现了如何经过 refs 访问到被包裹组件的实例方法和实例自己
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 <WrappedComponent {...props}/> } } } 复制代码
当被包裹组件被渲染,ref 回调就将执行,由此就能得到其实例的引用。这能够用于读取、增长实例属性,或调用实例方法。
经过提供给被包裹组件的属性和回调,能够抽象 state,这很是相似于 smart 组件是如何处理 dumb 组件的。
关于上述两种组件能够参阅:
https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0
例子:在下面这个抽象 state 的例子里咱们简单的将 value 和 onChange 处理函数从 name 输入框中抽象出来。之因此说“简单”是由于这很是广泛,但你必须明白这一点。
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}/>
}
}
复制代码
因而这个输入框就自动成为了一个受控组件。
关于受控组件:
https://mp.weixin.qq.com/s/I3aPxyZA_iArUDmsXtXGcw
能够利用组件的包裹,实现样式定义、布局或其余目标。一些基础用法能够由普通的父组件完成(参阅附录B),但如前所述,用 HOC 能够更加灵活。
例子:为定义样式而实现的包裹
function ppHOC(WrappedComponent) {
return class PP extends React.Component {
render() {
return (
<div style={{display: 'block'}}> <WrappedComponent {...this.props}/> </div> ) } } } 复制代码
继承反转 (Inheritance Inversion) 只须要这样实现就能够:
function iiHOC(WrappedComponent) {
return class Enhancer extends WrappedComponent {
render() {
return super.render()
}
}
}
复制代码
如你所见,被返回的 HOC 类(强化过的类)继承了被包裹的组件。之因此被称为“继承反转”是由于,被包裹组件并不去继承强化类,而是被动的让强化类继承。经过这种方式,两个类的关系看起来反转了。
继承反转使得 HOC 能够用 this
访问被包裹组件的实例,这意味着能够访问 state、props、组件生命周期钩子,以及 render 方法。
这里并不深刻探讨能够在生命周期钩子中实现的细节,由于那属于 React 的范畴。但要知道经过继承反转能够为被包裹组件建立新的生命周期钩子;并记住老是应该调用 super.[lifecycleHook]
以确保不会破坏被包裹的组件。
在深刻以前咱们大概说一下这些理论。
一致性比较
https://facebook.github.io/react/docs/reconciliation.html
React Elements 描述了 React 运行其一致性比较过程时,什么会被渲染。
React Elements 能够是两种类型:字符串和函数。字符串类型的 React Elements(STRE)表明 DOM 节点,函数类型的 React Elements(FTRE)表明继承自 React.Component 的组件。
React 元素和组件
https://facebook.github.io/react/blog/2015/12/18/react-components-elements-and-instances.html
在 React 的一致性比较过程(最终结果是 DOM 元素)中,FTRE 会被处理成一棵完整的 STRE 树。
之因此很重要,就在于这意味着继承反转高阶组件并不保证处理完整的子树。
后面学习到 render 劫持的时候将会证实其重要性。
称之为“render 劫持”是由于 HOC 控制了被包裹组件的 render 输出,并能对其作任何事情。
在 render 劫持中能够:
*用 render 引用被包裹组件的 render 方法
不能对被包裹组件的实例编辑或建立属性,由于一个 React Component 没法编辑其收到的 props,但能够改变被 render 方法输出的元素的属性。
就如咱们以前学到的,继承反转 HOC 不保证处理完整的子树,这意味着 render 劫持技术有一些限制。经验法则是,借助于 render 劫持,能够很少很多的操做被包裹组件的 render 方法输出的元素树。若是那个元素数包含了一个函数类型的 React Component,那就没法操做其子组件(被 React 的一致性比较过程延迟到真正渲染到屏幕上时)。
例子1:条件性渲染
function iiHOC(WrappedComponent) {
return class Enhancer extends WrappedComponent {
render() {
if (this.props.loggedIn) {
return super.render()
} else {
return null
}
}
}
}
复制代码
例子2:修改 render 输出的元素树
function iiHOC(WrappedComponent) {
return class Enhancer 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
}
}
}
复制代码
本例中,若是由 render 输出的被包裹组件有一个 input 顶级元素,就改变其 value。
能够在这里作任何事情,能够遍历整个元素树并改变其中的任何一个元素属性。
注意:不能经过属性代理劫持 render
虽然经过 WrappedComponent.prototype.render 访问 render 方法是可能的,但这样一来你就要模拟被包裹组件的实例及其属性,并本身处理组件生命周期而非依靠 React 去解决。以个人经验来讲这是得不偿失的,若是要劫持 render 应该用继承反转而非属性代理。要记住 React 内在地处置组件实例,而你只能经过
this
或 refs 来处理实例。
HOC 能够读取、编辑和删除被包裹组件实例的 state,也能够按需增长更多的 state。要谨记若是把 state 搞乱会很糟糕。大部分 HOC 应该限制读取或增长 state,然后者(译注:增长 state)应该使用命名空间以避免和被包裹组件的 state 搞混。
例子:对访问被包裹组件的 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 将被包裹组件嵌入其余元素中,并显示了其 props 和 state。
使用 HOC 时,就失去了被包裹组件原有的名字,可能会影响开发和调试。
人们一般的作法就是用原有名字加上些什么来命名 HOC。下面的例子取自 React-Redux
HOC.displayName = `HOC(${getDisplayName(WrappedComponent)})`
//or
class HOC extends ... {
static displayName = `HOC(${getDisplayName(WrappedComponent)})`
...
}
复制代码
而 getDisplayName 函数的定义以下:
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName ||
WrappedComponent.name ||
‘Component’
}
复制代码
其实你都不须要本身写一遍这个函数,recompose 库(https://github.com/acdlite/recompose)已经提供了。
如下为能够跳过的选读内容
在 HOC 中能够善用参数。这原本已经在上面全部例子中隐含的出现过,而且对于中级 JS 开发者也已经稀松日常了,可是本着知无不言的原则,仍是快速过一遍吧。
例子:结合属性代理和 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)
//or
@HOCFatoryFactory(params)
class WrappedComponent extends React.Component{}
复制代码
如下为能够跳过的选读内容
有一些子组件的 React 组件称为父组件,React 有一些访问和控制组件子成员的 API。
例子:父组件访问子组件
class Parent extends React.Component {
render() {
return (
<div>
{this.props.children}
</div>
)
}
}
}
render((
<Parent>
{children}
</Parent>
), mountNode)
复制代码
相比于 HOC,来细数一下父组件能作和不能作的:
一般,父组件的作法没有 HOC 那么 hacky,但上述列表是其相比于 HOC 的不灵活之处。
但愿阅读本文后你能对 React HOC 多一些了解。在不一样的库中,HOC 都被证实是颇有价值并不是常好用的。
React 带来了不少创新,人们普遍应用着 Radium、React-Redux、React-Router 等等,也很好的印证了这一点。
长按二维码或搜索 fewelife 关注咱们哦