前言
最近在学习 React 的封装,虽然日常的开发中也有用到 HOC 或者 Render Props,但从继承到组合,静态构建到动态渲染,都是似懂非懂,索性花时间系统性的整理,如有错误,请轻喷~~
例子
以下是 React 官方的一个例子,我会采用不同的封装方法来尝试代码复用,例子地址。
组件在 React 是主要的代码复用单元,但如何共享状态或一个组件的行为封装到其他需要相同状态的组件中并不是很明了。例如,下面的组件在 web 应用追踪鼠标位置:
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>Move the mouse around!</h1>
<p>The current mouse position is ({this.state.x}, {this.state.y})</p>
</div>
);
}
}
随着鼠标在屏幕上移动,在一个 <p> 的组件上显示它的 (x, y) 坐标。
现在的问题是:我们如何在另一个组件中重用行为?换句话说,若另一组件需要知道鼠标位置,我们能否封装这一行为以让能够容易在组件间共享?
由于组件是 React 中最基础的代码重用单元,现在尝试重构一部分代码能够在 <Mouse> 组件中封装我们需要在其他地方的行为。
// The <Mouse> component encapsulates the behavior we need…
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}>
{/* …but how do we render something other than a <p>? */}
<p>The current mouse position is ({this.state.x}, {this.state.y})</p>
</div>
);
}
}
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>Move the mouse around!</h1>
<Mouse />
</div>
);
}
}
现在 <Mouse> 组件封装了所有关于监听 mousemove 事件和存储鼠标 (x, y) 位置的行为,但其仍不失真正的可重用。
例如,假设我们现在有一个在屏幕上跟随鼠标渲染一张猫的图片的 <Cat> 组件。我们可能使用 <Cat mouse={{x, y}} prop 来告诉组件鼠标的坐标以让它知道图片应该在屏幕哪个位置。
首先,你可能会像这样,尝试在 <Mouse> 的内部的渲染方法 渲染 <Cat> 组件:
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 MouseWithCat 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}>
{/*
We could just swap out the <p> for a <Cat> here … but then
we would need to create a separate <MouseWithSomethingElse>
component every time we need to use it, so <MouseWithCat>
isn’t really reusable yet.
*/}
<Cat mouse={this.state} />
</div>
);
}
}
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>Move the mouse around!</h1>
<MouseWithCat />
</div>
);
}
}
这一方法对我们的具体用例来说能够生效,但我们却没法实现真正的将行为封装成可重用的方式的目标。现在,每次我们在不同的用例中想要使用鼠标的位置,我们就不得不创建一个新的针对那一用例渲染不同内容的组件 (如另一个关键的 <MouseWithCat>)。
Mixin
Mixin 概念
React Mixin 将通用共享的方法包装成 Mixins 方法,然后注入各个组件实现,事实上已经是不被官方推荐使用了,但仍然可以学习一下,了解其为什么被遗弃,先从 API 看起。React Mixin 只能通过 React.createClass() 使用,如下:
var mixinDefaultProps = {}
var ExampleComponent = React.createClass({
mixins: [mixinDefaultProps],
render: function(){}
});
Mixin 实现
// 封装的 Mixin
const mouseMixin = {
getInitialState() {
return {
x: 0,
y: 0
}
},
handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
})
}
}
const Mouse = createReactClass({
mixins: [mouseMixin],
render() {
return (
<div style={{height: ‘100%’}} onMouseMove={this.handleMouseMove}>
<p>The current mouse position is ({this.state.x}, {this.state.y})</p>
</div>
)
}
})
const Cat = createReactClass({
mixins: [mouseMixin],
render() {
return (
<div style={{height: ‘100%’}} onMouseMove={this.handleMouseMove}>
<img src=”/cat.jpg” style={{position: ‘absolute’, left: this.state.x, top: this.state.y}} alt=”” />
</div>
)
}
})
Mixin 的问题
然而,为什么 Mixin 会被不推荐使用?归纳起来就是以下三点
1. Mixin 引入了隐式依赖关系 如:
你可能会写一个有状态的组件,然后你的同事可能会添加一个读取这个状态的 mixin。在几个月内,您可能需要将该状态移至父组件,以便与兄弟组件共享。你会记得更新 mixin 来读取道具吗?如果现在其他组件也使用这个 mixin 呢?
2. Mixin 导致名称冲突 如:
你在该 Mixin 定义了 getSomeName, 另外一个 Mixin 又定义了同样的名称 getSomeName, 造成了冲突。
3. Mixin 导致复杂的滚雪球
随着时间和业务的增长,你对 Mixin 的修改越来越多,到最后会变成一个难以维护的 Mixin。
4. 拥抱 ES6,ES6 的 class 不支持 Mixin
HOC
HOC 概念
高阶组件(HOC)是 react 中的高级技术,用来重用组件逻辑。但高阶组件本身并不是 React API。它只是一种模式,这种模式是由 react 自身的组合性质必然产生的,是 React 社区发展中产生的一种模式。高阶组件的名称是从高阶函数来的,如果了解过函数式编程,就会知道高阶函数就是一个入参是函数,返回也是函数的函数,那么高阶组件顾名思义,就是一个入参是组件,返回也是组件的函数,如:
const EnhancedComponent = higherOrderComponent(WrappedComponent);
HOC 实现
高阶组件在社区中,有两种使用方式,分别是:
其中 W (WrappedComponent) 指被包裹的 React.Component,E (EnhancedComponent) 指返回类型为 React.Component 的新的 HOC。
Props Proxy:HOC 对传给 WrappedComponent W 的 porps 进行操作。
Inheritance Inversion:HOC 继承 WrappedComponent W。
依然是使用之前的例子,先从比较普通使用的 Props Proxy 看起:
class Mouse extends React.Component {
render() {
const {x, y} = this.props.mouse
return (
<p>The current mouse position is ({x}, {y})</p>
)
}
}
class Cat extends React.Component {
render() {
const {x, y} = this.props.mouse
return (
<img src=”/cat.jpg” style={{position: ‘absolute’, left: x, top: y}} alt=”” />
)
}
}
const MouseHoc = (MouseComponent) => {
return class 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}>
<MouseComponent mouse={this.state} />
</div>
)
}
}
}
const EnhanceMouse = MouseHoc(Mouse)
const EnhanceCat = MouseHoc(Cat)
那么在 Hoc 的 Props Proxy 模式下,我们可以做什么?
操作 Props 如上面的 MouseHoc, 假设在日常开发中,我们需要传入一个 props 给 Mouse 或者 Cat,那么我们可以在 HOC 里面对 props 进行增删查改等操作,如下:
const MouseHoc = (MouseComponent, props) => {
props.text = props.text + ‘—I can operate props’
return class extends React.Component {
……
render() {
return (
<div style={{height: ‘100%’}} onMouseMove={this.handleMouseMove}>
<MouseComponent {…props} mouse={this.state} />
</div>
)
}
}
}
MouseHoc(Mouse, {
text: ‘some thing…’
})
通过 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 就是我们的例子。
<MouseComponent mouse={this.state} />
包裹 WrappedComponent
<div style={{height: ‘100%’}} onMouseMove={this.handleMouseMove}>
<MouseComponent mouse={this.state} />
</div>
另外一种 HOC 模式则是 Inheritance Inversion,不过该模式比较少见,一个最简单的例子如下:
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 方法。
那么在我们的例子中它是这样的:
class Mouse extends React.Component {
render(props) {
const {x, y} = props.mouse
return (
<p>The current mouse position is ({x}, {y})</p>
)
}
}
class Cat extends React.Component {
render(props) {
const {x, y} = props.mouse
return (
<img src=”/cat.jpg” style={{position: ‘absolute’, left: x, top: y}} alt=”” />
)
}
}
const MouseHoc = (MouseComponent) => {
return class extends MouseComponent {
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() {
const props = {
mouse: this.state
}
return (
<div style={{height: ‘100%’}} onMouseMove={this.handleMouseMove}>
{super.render(props)}
</div>
)
}
}
}
const EnhanceMouse = MouseHoc(Mouse)
const EnhanceCat = MouseHoc(Cat)
同样,在 II 模式下,我们能做些什么呢?
渲染劫持 因为 render() 返回的就是 JSX 编译后的对象,如下:
可以通过手动修改这个 tree,来达到一些需求效果,不过这通常不会用到:
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
}
}
}
操作 state
HOC 可以读取、编辑和删除 WrappedComponent 实例的 state,如果你需要,你也可以给它添加更多的 state。记住,这会搞乱 WrappedComponent 的 state,导致你可能会破坏某些东西。要限制 HOC 读取或添加 state,添加 state 时应该放在单独的命名空间里,而不是和 WrappedComponent 的 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>
)
}
}
}
为什么有 Class 而不去使用继承返回来使用 HOC
可能有人看到这里会有疑惑,为什么有 Class 而不去使用继承返回来使用 HOC,这里推荐知乎的一个比较好的答案
OOP 和 FP 并不矛盾,所以混着用没毛病,很多基于 FP 思想的库也需要 OOP 来搭建。为什么 React 推崇 HOC 和组合的方式,我的理解是 React 希望组件是按照最小可用的思想来进行封装的,理想的说,就是一个组件只做一件的事情,且把它做好,DRY。在 OOP 原则,这叫单一职责原则。如果要对组件增强,首先应该先思路这个增强的组件需要用到哪些功能,这些功能由哪些组件提供,然后把这些组件组合起来.
D 中 A 相关的功能交由 D 内部的 A 来负责,D 中 B 相关的功能交由 D 内部的 B 来负责,D 仅仅负责维护 A,B,C 的关系,另外也可以额外提供增加项,实现组件的增强。
继承没有什么不好,注意,React 只是推荐,但没限制。其实用继承来扩展组件也没问题,而且也存在这样的场景。比如:有一个按钮组件,仅仅是对 Button 进行一个包装,我们且叫它 Button,可是,按照产品需求,很多地方的按钮都是带着一个 icon 的,我们需要提供一个 IconButton。这是时候,就可以通过继承来扩展,同时组合另外一个独立的组件,我们且叫它 Icon,显示 icon 的功能交给 Icon 组件来做,原来按钮的功能继续延续着。对于这种同类型组件的扩展,我认为用继承的方式是没关系的,灵活性,复用性还在。但是,用继承的方式扩展前,要先思考,新组件是否与被继承的组件是不是同一类型的,同一类职责的。如果是,可以继承,如果不是,那么就用组合。怎么定义同一类呢,回到上面的 Button 的例子,所谓同一类,就是说,我直接用 IconButton 直接替换掉 Button,不去改动其他代码,页面依然可以正常渲染,功能可以正常使用,就可以认为是同一类的,在 OOP 中,这叫做里氏替换原则。
继承会带来什么问题,以我的实践经验,过渡使用继承,虽然给编码带来便利,但容易导致代码失控,组件膨胀,降低组件的复用性。比如:有一个列表组件,叫它 ListView 吧,可以上下滚动显示一个 item 集,突然有一天需求变了,PM 说,我要这个 ListView 能像 iOS 那样有个回弹效果。好,用继承对这个 ListView 进行扩展,加入了回弹效果,任务 closed。第二天 PM 找上门来了,希望所有上下滚动的地方都可以支持回弹效果,这时候就懵逼啦,怎么办?把 ListView 中回弹效果的代码 copy 一遍?这就和 DRY 原则相悖了不是,而且有可能受到其他地方代码的影响,处理回弹效果略有不同,要是有一天 PM 希望对这个回弹效果做升级,那就有得改啦。应对这种场景,最好的办法是啥?用组合,封装一个带回弹效果的 Scroller,ListView 看成是 Scroller 和 item 容器组件的组合,其他地方需要要用到滚动的,直接套一个 Scroller,以后不管回弹效果怎么变,我只要维护这个 Scroller 就好了。当然,最理想的,把回弹效果也做成一个组件 SpringBackEffect,从 Scroller 分离出来,这样,需要用回弹效果的地方就加上 SpringBackEffect 组件就好了,这就是为什么组合优先于继承的原因。
页面简单的时候,组合也好,继承也罢,可维护就好,能够快速的响应需求迭代就好,用什么方式实现到无所谓。但如果是一个大项目,页面用到很多组件,或者是团队多人共同维护的话,就要考虑协作中可能存在的矛盾,然后通过一定约束来闭坑。组合的方式是可以保证组件具有充分的复用性,灵活度,遵守 DRY 原则的其中一种实践。
Mixin 和 HOC 的对比
Mixin 就像他的名字,他混入了组件中,我们很难去对一个混入了多个 Mixin 的组件进行管理,好比一个盒子,我们在盒子里面塞入了各种东西(功能),最后肯定是难以理清其中的脉络。HOC 则像是一个装饰器,他是在盒子的外面一层一层的装饰,当我们想要抽取某一层或者增加某一层都非常容易。
HOC 的约定
贯穿传递不相关 props 属性给被包裹的组件 高阶组件应该贯穿传递与它专门关注无关的 props 属性。
render() {
// 过滤掉专用于这个阶组件的 props 属性,
// 不应该被贯穿传递
const {extraProp, …passThroughProps} = this.props;
// 向被包裹的组件注入 props 属性,这些一般都是状态值或
// 实例方法
const injectedProp = someStateOrInstanceMethod;
// 向被包裹的组件传递 props 属性
return (
<WrappedComponent
injectedProp={injectedProp}
{…passThroughProps}
/>
);
}
最大化的组合性
// 不要这样做……
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))
// ……你可以使用一个函数组合工具
// compose(f, g, h) 和 (…args) => f(g(h(…args))) 是一样的
const enhance = compose(
// 这些都是单独一个参数的高阶组件
withRouter,
connect(commentSelector)
)
const EnhancedComponent = enhance(WrappedComponent)
包装显示名字以便于调试
最常用的技术是包裹显示名字给被包裹的组件。所以,如果你的高阶组件名字是 withSubscription,且被包裹的组件的显示名字是 CommentList,那么就是用 WithSubscription(CommentList) 这样的显示名字
function withSubscription(WrappedComponent) {
class WithSubscription extends React.Component {/* … */}
WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
return WithSubscription;
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || ‘Component’;
}
HOC 的警戒
不要在 render 方法内使用高阶组件,因为每次高阶组件返回的都是不同的组件,会造成不必要的渲染。
必须将静态方法做拷贝。
HOC 带来的问题:
当存在多个 HOC 时,你不知道 Props 是从哪里来的。
和 Mixin 一样,存在相同名称的 props,则存在覆盖问题,而且 react 并不会报错。
JSX 层次中多了很多层次(即无用的空组件),不利于调试。
HOC 属于静态构建, 静态构建即是重新生成一个组件,即返回的新组件,不会马上渲染,即新组件中定义的生命周期函数只有新组件被渲染时才会执行。
Render Props
Render Props 概念
Render Props 从名知义,也是一种剥离重复使用的逻辑代码,提升组件复用性的解决方案。在被复用的组件中,通过一个名为“render”(属性名也可以不是 render,只要值是一个函数即可) 的属性,该属性是一个函数,这个函数接受一个对象并返回一个子组件,会将这个函数参数中的对象作为 props 传入给新生成的组件。
Render Props 应用
可以看下最初的例子在 render props 中的应用:
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>Move the mouse around!</h1>
<Mouse render={mouse => (
<Cat mouse={mouse} />
)}/>
</div>
);
}
}
render props 的优势
不用担心 Props 是从哪里来的,它只能从父组件传递过来。
不用担心 props 的命名问题。
render props 是动态构建的。
动态构建和静态构建
这里简单的说下动态构建,因为 React 官方推崇动态组合,然而 HOC 实际上是一个静态构建,比如,在某个需求下,我们需要根据 Mouse 中某个字段来决定渲染 Cat 组件或者 Dog 组件,使用 HOC 会是如下:
const MouseHoc = (Component) => {
return Class extends React.Component {
render() {
return (
<div>
<h1>Move the mouse around!</h1>
{
isCat ? <Cat mouse={mouse} /> : <Dog mouse={mouse} />
}
</div>
);
}
}
}
可以看到,我们不得不提前静态构建好 Cat 和 Dog 组件
假如我们用 Render props:
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>Move the mouse around!</h1>
<Mouse render={(mouse, isCat) => (
isCat ? <Cat mouse={mouse} /> : <Dog mouse={mouse} />
)}/>
</div>
);
}
}
很明显,在动态构建的时候,我们具有更多的灵活性,我们可以更好的利用生命周期,相比较 HOC,就不得不引入 Cat 和 Dog 组件,污染了 MouseHoc。
Render Props 的缺点
无法使用 SCU 做优化,具体参考官方文档。
总结
抛开被遗弃的 Mixin 和尚未稳定的 Hooks,目前社区的代码复用方案主要还是 HOC 和 Render Props,个人感觉,如果是多层组合或者需要动态渲染那就选择 Render Props,而如果是诸如在每个 View 都要执行的简单操作,如埋点、title 设置等或者是对性能要求比较高如大量表单可以采用 HOC。
参考
Function as Child Components Not HOCs React 高阶组件和 render props 的适用场景有区别吗,还是更多的是个人偏好? 深入理解 React 高阶组件 高阶组件 -React 精读《我不再使用高阶组件》为什么 React 推崇 HOC 和组合的方式,而不是继承的方式来扩展组件?React 中的 Render Props 使用 Render props 吧!渲染属性 (Render Props)