跳到主要内容

组件复用

高阶组件(HOC)

高阶组件是 React 中用于复用组件逻辑的一种高级技巧. HOC 自身不是 React API 的一部分, 它是基于 React 特性的一种设计模式.

高阶组件是参数为组件, 返回值为新组件的函数:

const EnhancedComponent = higherOrderComponent(WrappedComponent);

典型的例子: Redux 的connect

实现高阶组件有两种方式:

  1. 属性代理(Props Proxy): HOC 对传给 WrappedComponent 的 props 进行操作
  2. 反向继承(Ingeritance Inversion): HOC 继承 WrappedComponent

在讨论实现之前, 有些设计上的约定来帮助我们写出合理的 HOC

约定和原则

  • 不要改变原始组件, 使用组合
  • 不将不相关的 props 传递给被包裹的组件:
  • 最大化可组合型: 函数柯里化等
  • 包装显示名称以便轻松调试: 一般可以用with开头的函数名称来命名
  • 不要再 render 方法中使用 hoc
  • 务必复制静态方法

属性代理

function ppHOC(WrappedComponent) {
return class PP extends React.Component {
render() {
return <WrappedComponent {...this.props} />;
}
};
}

HOC 在 render 方法中返回了一个WrappedComponent类型的React Element, 我们还传入例如 HOC 接收到的 props, 这就是名字 Props Proxy 的由来.

Props Proxy 可以做什么:

  • 操作 props
  • 通过 Refs 访问到组件实例
  • 提取 state
  • 用其他元素包裹 WrappedComponent

操作 Props

读取, 添加, 编辑, 删除传给 WrappedComponentprops, 当删除或者编辑重要的 props 时要小心, 要确保高阶组件的 props 不会破坏WrappedComponent(用命名空间).

function ppHOC(WrappedComponent) {
return class PP extends React.Component {
newProps = {
user: currentLoggedInUser
};
render() {
return <WrappedComponent {...this.props} {...this.newProps} />;
}
};
}

通过 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} />;
}
};
}

提取 state

你可以通过传入 props 和回调函数把 state 提取出来,类似于 smart component 与 dumb component。

提取 state 的例子:提取了 input 的 value 和 onChange 方法。这个简单的例子不是很常规,但足够说明问题。

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

function ppHOC(WrappedComponent) {
return class PP extends React.Component {
render() {
return (
<div style={{ display: 'block' }}>
<WrappedComponent {...this.props} />
</div>
);
}
};
}

反向继承

Inheritance Inversion (II) 的最简实现:

function iiHOC(WrappedComponent) {
return class Enhancer extends WrappedComponent {
render() {
return super.render();
}
};
}

在这里的代码中, HOC 类继承了 WrappedComponent, 之所以被称为 Ingeritance Inversion, 是因为 WrappedComponent 被 Enhancer 继承了, 而不是 WrappedComponent 继承了 Enhancer

反向继承允许 HOC 通过 this 访问到 WrappedComponent, 意味着它可以访问到 state, props, 组件生命周期方法和 render 方法.

通过这个可以创建新的生命周期方法, 为了不破坏 WrappedComponent, 请调用super.[lifecycleHook]

一致化处理(Reconciliation process)

React 在处理字符串类型的 React 元素, 函数类型的 React 元素是, 会进行一致化处理, 解析成一个完全有字符串类型 React 组件组成的树, 再转换为 DOM 元素, 这意味着 Inheritance Inversion 的高阶组件不一定会解析完整子树.

渲染劫持

之所以被称为渲染劫持, 是因为 HOC 控制了 WrappedComponent 的渲染输出, 可以使用它:

  • 在由 render 输出的任何 React 元素中读取、添加、编辑、删除 props
  • 读取和修改由 render 输出的 React 元素树
  • 有条件地渲染元素树
  • 把样式包裹进元素树(就像在 Props Proxy 中的那样)

例如: 条件渲染:

function iiHOC(WrappedComponent) {
return class Enhancer extends WrappedComponent {
render() {
if (this.props.loggedIn) {
return super.render();
} else {
return null;
}
}
};
}

修改由 render 方法输出的 React 组件树:

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;
}
};
}

在这个例子中,如果 WrappedComponent 的输出在最顶层有一个 input,那么就把它的 value 设为 “may the force be with you”。

你可以在这里做各种各样的事,你可以遍历整个元素树,然后修改元素树中任何元素的 props。这也正是样式处理库 Radium 所用的方法(案例分析一节中有更多关于 Radium 的信息)。

例子:通过访问 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 包裹一个组件会使其失去原本的组件名称, 可以这样:

HOC.displayName = `HOC(${getDisplayName(WrappedComponent)})`
//或
class HOC extends ... {
static displayName = `HOC(${getDisplayName(WrappedComponent)})`
...
}

function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName ||
WrappedComponent.name ||
‘Component’
}

渲染属性(Render Props)

具有 render prop 的组件接受一个函数,该函数返回一个 React 元素并调用它而不是实现自己的渲染逻辑。

<DataProvider render={data => <h1>Hello {data.target}</h1>} />

以复用一个鼠标事件为例:

class MouseTracker extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}

handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}

render() {
return (
<div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
<h1>移动鼠标!</h1>
<p>
当前的鼠标位置是 ({this.state.x}, {this.state.y})
</p>
</div>
);
}
}

现在复用和添加一些逻辑:

class Cat extends React.Component {
render() {
const mouse = this.props.mouse;
return <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />;
}
}

class Mouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}

handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}

render() {
return (
<div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
{/*
Instead of providing a static representation of what <Mouse> renders,
use the `render` prop to dynamically determine what to render.
*/}
{this.props.render(this.state)}
</div>
);
}
}

class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>移动鼠标!</h1>
<Mouse render={mouse => <Cat mouse={mouse} />} />
</div>
);
}
}

不同于 HOC 中的静态组合, render props 的组合是动态的, 每次组合都发生在 render 内部. 并且任何 HOC 都能使用 render props 替代.

const withMouse = Component => {
return class extends React.Component {
render() {
return <Mouse render={mouse => <Component {...this.props} mouse={mouse} />} />;
}
};
};

React Hook

React Hook 也能很好的进行组件的逻辑复用, 这里简单的举一个例子:

import React, { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);

function handleStatusChange(status) {
setIsOnline(status.isOnline);
}

useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});

return isOnline;
}

这个封装的逻辑, 可以的多个组件内重复的使用:

function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);

if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);

return <li style={{ color: isOnline ? 'green' : 'black' }}>{props.friend.name}</li>;
}

并且这两个 state 是完全独立的, 因为 Hook 是一种状态逻辑的方式, 并不是对数据本身的复用

Mixin

最后提一下已经不建议使用 Mixin 方法:

import React from 'react';
import ReactDOM from 'react-dom';
// mixin 中含有了你需要在任何应用中追踪鼠标位置的样板代码。
// 我们可以将样板代码放入到一个 mixin 中,这样其他组件就能共享这些代码
const MouseMixin = {
getInitialState() {
return { x: 0, y: 0 };
},
handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}
};
const App = React.createClass({
// 使用 mixin!
mixins: [MouseMixin],

render() {
const { x, y } = this.state;
return (
<div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
<h1>
The mouse position is ({x}, {y})
</h1>
</div>
);
}
});
ReactDOM.render(<App />, document.getElementById('app'));

mixin 不建议被使用一方面是因为官方使用 class 作为创建组件的方法后, ES6 class 不支持 mixins, 并且 mixin 改变了 state, 此外还可能存在名字冲突等问题.

小结

mixin

  • 组件与 mixin 之间是隐式依赖: 导致难以快速理解组件行为, 组件自身的 state 不能轻易修改
  • 多个 mixin 可能会冲突
  • mixin 倾向于增加更多状态, 增加复杂度, 降低可预测性

HOC

  • HOC 通过 props 影响内部组件状态, 降低了耦合度
  • 具有天然的层级结构, 降低了复杂度
  • 无法从外部访问子组件的 state, 因为无法通过 shouldComponentUpdate 过滤不必要的更新(PureComponent)
  • Ref 被隔断(forwardRef)
  • 外层包装组件是不可见的

Render Props

  • 解决的 HOC 的一些问题
  • 将组件嵌套转换为了函数回调的嵌套

React Hook

  • 解决嵌套问题, 使用更加简洁
  • 更方便的将逻辑与 UI 分离
  • 可以使用 Hook 组合
  • 复用成本低
  • 额外的学习成本
  • 破坏了 PureComponent,React.memo 的浅比较的性能优化效果(为了取最新的 props 和 state,每次 render()都要重新创建事件处函数)
  • 在闭包场景可能会引用到旧的 state、props 值
  • 内部实现上不直观(依赖一份可变的全局状态,不再那么“纯”)
  • React.memo 并不能完全替代 shouldComponentUpdate(因为拿不到 state change,只针对 props change)

参考链接