React编程中的一个核心思想就是组件化,通过对状态和逻辑的封装,提高组件的复用性。组件作为一个独立的解耦模块,可以有效降低测试难度。从React诞生到发展至今,关于逻辑复用的方案,社区和官方都给出了很多不同的方案。这里总结一下,在React中,代码复用的几种方案及其各自的利弊:
Mixin
了解过javascript设计模式的同学,可能对这个词语会比较熟悉,Mixin其实就是混入的方式,将功能注入到原型对象中,来实现代码和逻辑的复用。但是副作用也是很明显的,他会对原有的对象造成污染,函数来源不确定,并且可能出现命名冲突的问题。
对于刚接触react开发不久的同学,可能都没有听说过这个方案,但是在React比较早期的版本,大概在15年那会,mixin方案还是很流行的。
我们通过一个简单的例子,看一下在React中,如何使用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'))
我们创建了一个MouseMixin,然后在react.createClass 这个范式中,将该mixin引入,这样任何组件都可以复用该逻辑,追踪鼠标的位置。
但是细心观察可以发现,这种方式其实有很多的弊端:
- 他打破了原有组件的封装
- 可能会出现命名冲突的问题
- 增加复杂度
- ES6 不支持
HOC
随着ES6 Class
的到来,React团队最终决定使用ES6 Class代替原有的createClass。既然mixin有这么多的弊端,并且ES6不支持,那么新的复用方案呼之欲出,那就是HOC
.
所谓HOC,就是指高阶组件,他的概念类似于高阶函数,只是这里接受一个组件作为参数,经过装饰之后,返回一个新的组件。装饰的过程,就是将可复用的逻辑,附加到原有的组件上,可以是组件结构的扩展,也可以是功能的扩充。
HOC模式在抽离公用逻辑的同时,减少对原有组件的侵入性,非常适合在开发过程中业务模块的封装与复用。但是他同样会有一些隐藏的问题:
- 命名冲突。 HOC通过props向被装饰的组件传递数据,如果多个HOC装饰同一个组件的话,就可能出现命名冲突
- 不够直接。我们从props中接受的数据,有时候不知道是从那个HOC传递过来的。虽然语义化的命名可能会解决这个问题,但是让规约来弥补设计方案的缺陷,本身就是不太合理的。
虽然HOC存在一些以上隐藏的问题,但是社区以及平时的业务开发中,还是很流行的。
另外,HOC和mixin两者都存在的一个问题是,他们都是静态组合,而非动态组合。在类被创建的时候,HOC和mixin就生效了,而非是在运行态。一会儿我将看一下,render props是如何实现动态组合的
Render Props
Render Props是一个类型为函数的props,来实现组件之间的代码复用。
使用该方案解决上述问题的代码如下:
import React from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'
// 与 HOC 不同,我们可以使用具有 render prop 的普通组件来共享代码
class Mouse extends React.Component {
static propTypes = {
render: PropTypes.func.isRequired
}
state = { x: 0, y: 0 }
handleMouseMove = (event) => {
this.setState({
x: event.clientX,
y: event.clientY
})
}
render() {
return (
<div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
{this.props.render(this.state)}
</div>
)
}
}
const App = React.createClass({
renderTitle =({x, y})=>{
return
// render prop 给了我们所需要的 state 来渲染我们想要的
<h1>The mouse position is ({x}, {y})</h1>
}
render() {
return (
<div style={{ height: '100%' }}>
<Mouse render={this.renderTitle}/>
</div>
)
}
})
Mouse 组件接受一个类型为函数的render props,然后在render中调用该方法,并将自身的state作为参数,这样,其他组件就能到Mouse的状态,并渲染想要的内容。并且我们注意到,组合是发生在render中的,也就是说在运行态下的动态组合,我们就可以用到原组件内部的任何state 或 props数据,以及生命周期函数等等。
该方案也规避了上述提到的其他缺陷。
Hooks
Hooks是React官方提出的一个全新的概念,旨在解决React开发过程中遇到的一系列问题,其中就包含上面提到的逻辑复用。关于该问题,官方文档中提出,之前你可以会用HOC或者 render props来试图解决,但是这种模式需要对组件进行重组,未免显得有点麻烦,并且会造成组件的嵌套深渊,让你在调试过程中难以追踪。
Hooks可以帮助你将状态逻辑从组件中抽离出来,在不需要改变组件DOM结构层次的情况下,实现逻辑复用。
如果采用Hooks的话,代码实现如下:
aaa