[react] 高阶组件 HOC (2)
高阶组件 HOC(Higher Order Component)
hocFactory:: W: React.Component => E: React.Component
这种模式通常使用函数来实现,基本上是一个类工厂。
有两种实现方式:
- Props Proxy: HOC 对传给 WrappedComponent W 的 porps 进行操作
- Inheritance Inversion: HOC 继承 WrappedComponent W。
1 Props Proxy
React-Redux 的 connect
,处理监听 store 和后续的处理,是通过 Props Proxy 来实现的。
Props Proxy (PP) 的最简实现:
HOC 在 render 方法中 返回 了一个 WrappedComponent 类型的 React Element。
还传入了 HOC 接收到的 props,这就是名字 Props Proxy 的由来。
function ppHOC(WrappedComponent) {
return class PP extends React.Component {
render() {
return <WrappedComponent {...this.props}/>
}
}
}
1.1 操作 props
添加新的 props。
当前登录的用户可以在 WrappedComponent 中通过 this.props.user 访问到。
function PPHOC(WrappedComponent) {
return class PP extends React.Component {
render() {
const props = Object.assign({}, this.props, {
user: {
name: 'xx',
email: 'xx@xx.com'
}
})
return <WrappedComponent {...props}/>
}
}
}
class Example extends React.Component {
render() {
return (
<div>
<p>
As you can see, all original props (date), are being passed through or proxied,
and also new props (user) are being added.
</p>
<pre>{JSON.stringify(this.props, null, 2)}</pre>
</div>
)
}
}
const EnhancedExample = PPHOC(Example)
ReactDOM.render(<EnhancedExample date={(new Date).toISOString()}/>, document.getElementById('root'))
1.2 通过 Refs 访问到组件实例
可以通过引用(ref)访问到 this (WrappedComponent 的实例),但为了得到引用,
WrappedComponent 还需要一个初始渲染,
意味着你需要在 HOC 的 render 方法中返回 WrappedComponent 元素,
让 React 开始它的一致化处理,你就可以得到 WrappedComponent 的实例的引用。
通过 refs 访问到实例的方法和实例本身。
Ref 的回调函数会在 WrappedComponent 渲染时执行,就可以得到 WrappedComponent 的引用。
这可以用来读取/添加实例的 props ,调用实例的方法。
function refsHOC(WrappedComponent) {
return class RefsHOC extends React.Component {
proc=(wrappedComponentInstance)=> {
wrappedComponentInstance.method()
}
render() {
const props = Object.assign({}, this.props, {ref: this.proc})
return <WrappedComponent {...props}/>
}
}
}
1.3 提取 state
可以通过传入 props 和回调函数把 state 提取出来,类似于 smart component 与 dumb component。
提取了 input 的 value 和 onChange 方法。
function PPHOC(WrappedComponent) {
return class PP extends React.Component {
constructor(props) {
super(props)
this.state = { fields: {} }
}
getField=(fieldName)=> {
if (!this.state.fields[fieldName]) {
this.state.fields[fieldName] = {
value: '',
onChange: event => {
this.state.fields[fieldName].value = event.target.value
this.forceUpdate()
}
}
}
return {
value: this.state.fields[fieldName].value,
onChange: this.state.fields[fieldName].onChange
}
}
render() {
const props = Object.assign({}, this.props, {
fields: this.getField,
})
return (
<div>
<h2>
PP HOC
</h2>
<p>Im a Props Proxy HOC that abstracts controlled inputs</p>
<WrappedComponent {...props}/>
</div>
)
}
}
}
class Example extends React.Component {
render() {
return (
<div>
<h2>
Wrapped Component
</h2>
<p>
Props
</p>
<pre>{stringify(this.props)}</pre>
<form>
<label>Automatically controlled input!</label>
<input type="email" {...this.props.fields('email')}/>
</form>
</div>
)
}
}
const EnhancedExample = DebuggerHOC(PPHOC(Example))
ReactDOM.render(<EnhancedExample date={(new Date).toISOString()}/>, document.getElementById('root'))
1.4 用其他元素包裹 WrappedComponent
为了封装样式、布局或别的目的,可以用其它组件和元素包裹 WrappedComponent。
基本方法是使用父组件实现,但通过 HOC 可以得到更多灵活性。
包裹样式
function ppHOC(WrappedComponent) {
return class PP extends React.Component {
render() {
return (
<div style={{display: 'block'}}>
<WrappedComponent {...this.props}/>
</div>
)
}
}
}
2 Inheritance Inversion
Radium 的内联样式是通过用 Inheritance Inversion 模式做到了渲染劫持。
Inheritance Inversion (II) 的最简实现:
function iiHOC(WrappedComponent) {
return class Enhancer extends WrappedComponent {
render() {
return super.render()
}
}
}
返回的 HOC 类(Enhancer)继承了 WrappedComponent。
之所以被称为 Inheritance Inversion 是因为 WrappedComponent 被 Enhancer 继承了,
而不是 WrappedComponent 继承了 Enhancer。
在这种方式中,它们的关系看上去被反转(inverse)了。
Inheritance Inversion 允许 HOC 通过 this 访问到 WrappedComponent,
意味着它可以访问到 state、props、组件生命周期方法和 render 方法。
通过 II 可以创建新的生命周期方法。
为了不破坏 WrappedComponent,记得调用 super.[lifecycleHook]
。
一致化处理(Reconciliation process)
React 元素决定描述了在 React 执行一致化处理时它要渲染什么。
React 元素有两种类型:字符串和函数。
字符串类型的 React 元素代表 DOM 节点,函数类型的 React 元素代表继承 React.Component 的组件。
函数类型的 React 元素会在一致化处理中被解析成一个完全由字符串类型 React 组件组成的树(而最后的结果永远是 DOM 元素)。
这很重要,意味着 Inheritance Inversion 的高阶组件不一定会解析完整子树
Inheritance Inversion 的高阶组件不一定会解析完整子树。
2.1 渲染劫持
之所以被称为渲染劫持是因为 HOC 控制着 WrappedComponent 的渲染输出,可以用它做各种各样的事。
通过渲染劫持可以:
在由 render输出的任何 React 元素中读取、添加、编辑、删除 props
读取和修改由 render 输出的 React 元素树
有条件地渲染元素树
把样式包裹进元素树(就像在 Props Proxy 中的那样)
*render 指 WrappedComponent.render 方法
你不能编辑或添加 WrappedComponent 实例的 props,因为 React 组件不能编辑它接收到的 props,但你可以修改由 render 方法返回的组件的 props。
II 类型的 HOC 不一定会解析完整子树,意味着渲染劫持有一些限制。
使用渲染劫持可以完全操作 WrappedComponent 的 render 方法返回的元素树。
但是如果元素树包括一个函数类型的 React 组件,就不能操作它的子组件了。(被 React 的一致化处理推迟到了真正渲染到屏幕时)
条件渲染。
当 this.props.loggedIn 为 true 时,这个 HOC 会完全渲染 WrappedComponent 的渲染结果。
(假设 HOC 接收到了 loggedIn 这个 prop)
function iiHOC(WrappedComponent) {
return class Enhancer extends WrappedComponent {
render() {
if (this.props.loggedIn) {
return super.render()
} else {
return null
}
}
}
}
修改由 render 方法输出的 React 组件树。
如果 WrappedComponent 的输出在最顶层有一个 input,那么就把它的 value 设为 “may the force be with you”。
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
}
}
}
2.2 操作 state
HOC 可以读取、编辑和删除 WrappedComponent 实例的 state,如果需要,也可以给它添加更多的 state。
但是这会搞乱 WrappedComponent 的 state,可能会导致破坏某些东西。
要限制 HOC 读取或添加 state,添加 state 时应该放在单独的命名空间里,而不是和 WrappedComponent 的 state 混在一起。
通过访问 WrappedComponent 的 props 和 state 来做调试。
这里 HOC 用其他元素包裹着 WrappedComponent,还输出了 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>
)
}
}
}
3 命名
用 HOC 包裹一个组件会使它失去原本 WrappedComponent 的名字,可能会影响开发和调试。
通常会用 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 提供了这个函数。