共计 7298 个字符,预计需要花费 19 分钟才能阅读完成。
为了实现分离业务逻辑代码,实现组件内部相关业务逻辑的复用,在 React 的迭代中针对类组件中的代码复用依次发布了 Mixin、HOC、Render props 等几个方案。此外,针对函数组件,在 React v16.7.0-alpha 中提出了 hooks 的概念,在本身无状态的函数组件,引入独立的状态空间,也就是说在函数组件中,也可以引入类组件中的 state 和组件生命周期,使得函数组件变得丰富多彩起来,此外,hooks 也保证了逻辑代码的复用性和独立性。
本文从针对类组件的复用解决方案开始说起,先后介绍了从 Mixin、HOC 到 Render props 的演进,最后介绍了 React v16.7.0-alpha 中的 hooks 以及自定义一个 hooks
Mixin
HOC
Render props
React hooks 的介绍以及如何自定义一个 hooks
原文地址在我的博客中:https://github.com/forthealll…
欢迎 star 和 fork~
一、Mixin
Mixin 是最早出现的复用类组件中业务逻辑代码的解决方案,首先来介绍以下如何适应 Mixin。下面是一个 Mixin 的例子:
const someMixins={
printColor(){
console.log(this.state.color);
}
setColor(newColor){
this.setState({color:newColor})
}
componentDidMount(){
..
}
}
下面是一个使用 Mixin 的组件:
class Apple extends React.Component{
// 仅仅作为演示,mixins 一般是通过 React.createClass 创建,并且 ES6 中没有这种写法
mixins:[someMixins]
constructor(props){
super(props);
this.state={
color:’red’
}
this.printColor=this.printColor.bind(this);
}
render(){
return <div className=”m-box” onClick={this.printColor}>
这是一个苹果
</div>
}
}
在类中 mixin 引入公共业务逻辑:
mixins:[someMixins]
从上面的例子,我们来总结以下 mixin 的缺点:
Mixin 是可以存在多个的,是一个数组的形式,且 Mixin 中的函数是可以调用 setState 方法组件中的 state 的,因此如果有多处 Mixin 的模块中修改了相同的 state,会无法确定 state 的更新来源
ES6 classes 支持的是继承的模式,而不支持 Mixins
Mixin 会存在覆盖,比如说两个 Mixin 模块,存在相同生命周期函数或者相同函数名的函数,那么会存在相同函数的覆盖问题。
Mixin 已经被废除,具体缺陷可以参考 Mixins Considered Harmful
二、HOC
为了解决 Mixin 的缺陷,第二种解决方案是高阶组件(high order component, 简称 HOC)。
1、举例几种 HOC 的形式
HOC 简单理解就是组件工厂,接受原始组件作为参数,添加完功能与业务后,返回新的组件。下面来介绍 HOC 参数的几个例子。
(1)参数仅为原始组件
const redApple = withFruit(Apple);
(2)参数为原始组件和一个对象
const redApple = withFruit(Apple,{color:’red’,weight:’200g’});
但是这种情况比较少用,如果对象中仅仅传递的是属性,其实完全可以通过组件的 props 实现值的传递,我们用 HOC 的主要目的是分离业务,关于 UI 的展示,以及一些组件中的属性和状态,我们一般通过 props 来指定比较方便
(3)参数为原始组件和一个函数
const redApp=withFruit(App,()=>{console.log(‘I am a fruit’)})
(4)柯里化
最常见的是仅以一个原始组件作为参数,但是在外层包裹了业务逻辑,比如 react-redux 的 conect 函数中:
class Admin extends React.Component{
}
const mapStateToProps=(state)=>{
return {
};
}
const mapDispatchToProps=(dispatch)=>{
return {
}
}
const connect(mapStateToProps,mapDispatchToProps)(Admin)
2、HOC 的缺点
HOC 解决了 Mixin 的一些缺陷,但是 HOC 本身也有一些缺点:
(1)难以溯源,且存在属性覆盖问题
如果原始组件 A,先后通过工厂函数 1,工厂函数 2,工厂函数 3…. 构造,最后生成了组件 B,我们知道组件 B 中有很多与 A 组件不同的 props,但是我们仅仅通过组件 B,并不能知道哪个组件来自于哪个工厂函数。同时,如果有 2 个工厂函数同时修改了组件 A 的某个同名属性,那么会有属性覆盖的问题,会使得前一个工厂函数的修改结果失效。
(2)HOC 是静态构建的
所谓静态构建,也就是说生成的是一个新的组件,并不会马上 render,HOC 组件工厂是静态构建一个组件,这类似于重新声明一个组件的部分。也就是说,HOC 工厂函数里面的声明周期函数,也只有在新组件被渲染的时候才会执行。
(3)会产生无用的空组件
三、Render Prop
Render Props 从名知义,也是一种剥离重复使用的逻辑代码,提升组件复用性的解决方案。在被复用的组件中,通过一个名为“render”(属性名也可以不是 render,只要值是一个函数即可)的属性,该属性是一个函数,这个函数接受一个对象并返回一个子组件,会将这个函数参数中的对象作为 props 传入给新生成的组件。
这种方法跟直接的在父组件中,将父组件中的 state 直接传给子组件的区别是,通过 Render Props 不用写死子组件,可以动态的决定父组件需要渲染哪一个子组件。
或者再概括一点:
Render Props 就是一个函数,做为一个属性被赋值给父组件,使得父组件可以根据该属性去渲染子组件。
(1)标准父子组件通信方法
首先来看常用的在类组件中常用的父子组件,父组件将自己的状态 state,通过 props 传递给子组件。
class Son extends React.Component{
render(){
const {feature} = this.props;
return <div>
<span>My hair is {feature.hair}</span>
<span>My nose is {feature.nose}</span>
</div>
}
}
class FatherToSon extends React.Component{
constructor(){
this.state = {
hair:’black’,
nose:’high’
}
}
render(){
return <Son feature = {this.state}>
}
}
我们定义了父组件 FatherToSon,存在自身的 state,并且将自身的 state 通过 props 的方式传递给了子组件。
这种就是常见的利用组件的 props 父子间传值的方式,这个值可以是变量,对象,也可以是方法,但是仅仅使用只能一次性的给特定的子组件使用。如果现在有个 Daughter 组件也想复用父组件中的方法或者状态,那么必须新构建一个新组件:
class FatherToDaughter extends React.Component{
constructor(){
this.state = {
hair:’black’,
nose:’high’
}
}
render(){
return <Daughter feature = {this.state}>
}
}
从上面的例子可以看出通过标准模式的父子组件的通信方法,虽然能够传递父组件的状态和函数,但是无法实现复用。
(2)Render Props 的引出
我们根据 Render Props 的特点:
Render Props 就是一个函数,做为一个属性被赋值给父组件,使得父组件可以根据该属性去渲染子组件。
重新去实现上述的 (1) 中的例子。
class FatherChild extends React.Component{
constructor(){
this.state = {
hair:’black’,
nose:’high’
}
}
render(){
<React.Fragment>
{this.props.render}
</React.Fragment>
}
}
此时如果子组件要复用父组件中的属性或者函数,则可以直接使用,比如子组件 Son 现在可以直接调用:
<FatherChild render={(obj)=>(<Son feature={obj}>)} />
如果子组件 Daughter 要复用父组件的方法,可以直接调用:
<FatherChild render={(obj)=>(<Daughter feature={obj}>)} />
从这个例子中可以看出,通过 Render Props 我们实现同样实现了一个组件工厂,可以实现业务逻辑代码的复用,相比与 HOC,Render Props 有以下几个优点。
不用担心 props 的命名问题
可以溯源,子组件的 props 一定是来自于直接父组件
是动态构建的
Render Props 也有一个缺点:
就是无法利用 SCU 这个生命周期,来实现渲染性能的优化。
四、React hooks 的介绍以及如何自定义一个 hooks
hooks 概念在 React Conf 2018 被提出来,并将在未来的版本中被引入,hooks 遵循函数式编程的理念,主旨是在函数组件中引入类组件中的状态和生命周期,并且这些状态和生命周期函数也可以被抽离,实现复用的同时,减少函数组件的复杂性和易用性。
hooks 相关的定义还在 beta 中,可以在 React v16.7.0-alpha 中体验,为了渲染 hooks 定义的函数组件,必须执行 React-dom 的版本也为 v16.7.0-alpha,引入 hooks 必须先安装:
npm i -s React@16.7.0-alpha
npm i -s React-dom@16.7.0-alpha
hooks 主要有三部分组成,State Hooks、Effect Hooks 和 Custom Hooks,下面分别来一一介绍。
(1)State Hooks
跟类组件一样,这里的 state 就是状态的含义,将 state 引入到函数组件中,同时类组件中更新 state 的方法为 setState,在 State Hooks 中也有相应的更新状态的方法。
function ExampleWithManyStates() {
// 声明各种 state 以及更新相应的 state 的方法
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState(‘banana’);
const [todos, setTodos] = useState([{text: ‘Learn Hooks’}]);
// …
}
上述就声明了 3 个 State hooks,相应的方法为 useState,该方法创建一个传入初始值,创建一个 state。返回一个标识该 state 的变量,以及更新该 state 的方法。
从上述例子我们来看,一个函数组件是可以通过 useState 创建多个 state 的。此外 State Hooks 的定义必须在函数组件的最高一级,不能在嵌套,循环等语句中使用。
function ExampleWithManyStates() {
// 声明各种 state 以及更新相应的 state 的方法
if(Math.random()>1){
const [age, setAge] = useState(42);
const [todos, setTodos] = useState([{text: ‘Learn Hooks’}]);
}else{
const [fruit, setFruit] = useState(‘banana’);
const [todos, setTodos] = useState([{text: ‘Learn Hooks’}]);
}
// …
}
上述的方式是不被允许的,因为一个函数组件可以存在多个 State Hooks,并且 useState 返回的是一个数组,数组的每一个元素是没有标识信息的,完全依靠调用 useState 的顺序来确定哪个状态对应于哪个变量,所以必须保证使用 useState 在函数组件的最外层,此外后面要介绍的 Effect Hooks 的函数 useEffect 也必须在函数组件的最外层,之后会详细解释。
(2)Effect Hooks
通过 State Hooks 来定义组件的状态,同样通过 Effect Hooks 来引入生命周期,Effect hooks 通过一个 useEffect 的方法,以一种极为简化的方式来引入生命周期。来看一个更新的例子:
import {useState, useEffect} from ‘react’;
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
上述就是一个通过 useEffect 来实现组件中生命周期的例子,useEffect 整合了 componentDidMount 和 componentDidUpdate,也就是说在 componentDidMount 和 componentDidUpdate 的时候都会执行一遍 useEffect 的函数,此外为了实现 componentWillUnmount 这个生命周期函数,useEffect 函数如果返回值是一个函数,这个函数就被定义成在 componentWillUnmount 这个周期内执行的函数。
useEffect(() => {
//componentDidMount 和 componentDidUpdate 周期的函数体
return ()=>{
//componentWillUnmount 周期的函数体
}
});
如果存在多个 useState 和 useEffect 时,必须按顺序书写,定义一个 useState 后,紧接着就使用一个 useEffect 函数。
useState(‘Mary’)
useEffect(persistForm)
useState(‘Poppins’)
useEffect(updateTitle)
因此通 useState 一样,useEffect 函数也必须位于函数组件的最高一级。
(3)Effect Hooks 的补充
上述我们知道 useEffect 其实包含了 componentDidMount 和 componentDidUpdate,如果我们的方法仅仅是想在 componentDidMount 的时候被执行,那么必须传递一个空数组作为第二个参数。
useEffect(() => {
// 仅在 componentDidMount 的时候执行
},[]);
上述的方法会仅仅在 componentDidMount,也就是函数组件第一次被渲染的时候执行,此后及时状态更新,也不会执行。
此外,为了减少不必要的状态更新和渲染,可以如下操作:
useEffect(() => {
// 仅在 componentDidMount 的时候执行
},[stateName]);
在上述的这个例子中,只有 stateName 的值发生改变,才会去执行 useEffect 函数。
(4)Custom Hooks 自定义 hooks
可以将 useState 和 useEffect 的状态和生命周期函数抽离,组成一个新的函数,该函数就是一个自定义的封装完毕的 hooks。
这是我写的一个 hooks —> dom-location,
可以这样引入:
npm i -s dom-location
并且可以在函数组件中使用。这个自定义的 hooks 也很简单,就是封装了状态和生命周期函数。
import {useState, useEffect} from ‘react’
const useDomLocation = (element) => {
let [elementlocation,setElementlocation] = useState(getlocation(element));
useEffect(()=>{
element.addEventListener(‘resize’,handleResize);
return ()=>{
element.removeEventListener(‘resize’, handleResize);
}
},[]);
function handleResize(){
setElementlocation(getlocation(element));
}
function getlocation(E){
let rect = E.getBoundingClientRect()
let top = document.documentElement.clientTop
let left= document.documentElement.clientLeft
return{
top : rect.top – top,
bottom : rect.bottom – top,
left : rect.left – left,
right : rect.right – left
};
}
return elementlocation
}
然后直接在函数中使用:
import useDomLocation from ‘dom-location’;
function App() {
….
let obj = useDomLocation(element);
}