一、React Hook出现的背景
1.如何理解【有状态组件】和【无状态组件】
我们知道react最开始对class和函数组件的定位分别是有状态组件和无状态组件。具体表现在继承class类创建的组件和纯函数组件。
所谓的状态就是组件内部维护数据的state。因此有状态组件就是组件内有state,无状态组件就是组件内没有state。
无状态组件:无状态组件(Stateless Component)是最基础的组件形式,由于没有状态的影响所以就是纯静态展示的作用。一般来说,各种UI库里也是最开始会开发的组件类别。如按钮、标签、输入框等。它的基本组成结构就是属性(props)加上一个渲染函数(render)。由于不涉及到状态的更新,所以这种组件的复用性也最强。
有状态组件:在无状态组件的基础上,如果组件内部包含状态(state)且状态随着事件或者外部的消息而发生改变的时候,这就构成了有状态组件(Stateful Component)。有状态组件通常会带有生命周期(lifecycle),用以在不同的时刻触发状态的更新。这种组件也是通常在写业务逻辑中最经常使用到的,根据不同的业务场景组件的状态数量以及生命周期机制也不尽相同。
总结:在React中,我们通常通过props和state来处理两种类型的数据。
无状态组件(也称为哑组件)使用props来存储数据,props是只读的,只能由父组件设置。
有状态组件(也称为智能组件)使用state来存储数据。
2.为什么函数组件内部无法定义和维护state
首先维护状态不是函数中的维护变量,
import { sculptureList } from './data.js';
export default function Gallery() {
let index = 0;
function handleClick() {
index = index + 1;
}
let sculpture = sculptureList[index];
return (
<>
<h3>
{index + 1}
</h3>
<button onClick={handleClick}>
Next
</button>
</>
);
}
如下所示,在函数组件中定义了变量,也在click时候改变了变量。但是视图上并没有更新。而我们看下class组件是如何维护state的:
1.先将变量存储在state中(这一点函数组件可以做到);
2.更改变量是使用类组件提供的API:setState,这样才能保证数据的更新会同步到视图中(这个setState函数组件中没有提供)。因此函数组件无法实现:变量改变同步更新到视图。
class Counter1 extends React.Component {
constructor() {
this.state = {};
}
increment = () => {
this.setState(prevState => ({ count: prevState.count + 1 }));
};
render() {
return (
<div>
<p>Counter 1: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
总结:
函数组件内部无法定义和维护state的原因是:函数组件中没有提供像class组件那样的可以讲数据更新同步到视图的API:setState。
函数组件是无状态组件的特点:
1.表现在无法具有类组件维护state的能力
2.原因是没有提供类似setState那样的API,实现数据的更新同步到视图。
3.class类组件存在的问题
3.1 “组件”逻辑复用困难问题
首先声明一点,这里的要复用的逻辑是指什么?是不是utils中纯函数逻辑?
显然,utils中放的是纯函数逻辑,如果组件要复用就直接引用即可复用。不存在苦难的说法。所谓复用苦难,就是说不能复用,难以复用。所以这里的针对的复用逻辑是“组件逻辑”。什么是“组件逻辑”?
组件逻辑是指class类组件才有的特定逻辑(生命周期逻辑 && 状态逻辑-对state定义和处理)。
1.【场景1】:对组件中生命周期复用。类组件中特有生命周期,函数组件中不存在生命周期,而我们往往在生命周期中会调用fetch请求,并处理一些数据逻辑。那么如果存在一个场景:组件A和组件B都要在生命周期中调用相同的fetch,请问怎么样才能复用? 显然,我们需要把对应的生命周期抽出来,提供复用。
2.【场景2】:对组件中state状态定义&处理逻辑复用。如下所示:我们定义了两个组件,两个组件中存在的公共逻辑是:
(1) 定义state状态并初始化count值为0;
(2) 绑定一个事件并在组件内定义一个事件函数 (3) 定义事件函数内填写处理逻辑:处理逻辑是-每次+1class Counter1 extends React.Component { state = { count: 0 }; increment = () => { this.setState(prevState => ({ count: prevState.count + 1 })); }; render() { return ( <div> <p>Counter 1: {this.state.count}</p> <button onClick={this.increment}>Increment</button> </div> ); } } class Counter2 extends React.Component { state = { count: 0 }; increment = () => { this.setState(prevState => ({ count: prevState.count + 1 })); }; render() { return ( <div> <p>Counter 2: {this.state.count}</p> <button onClick={this.increment}>Increment</button> </div> ); } }此时,假设我们能做的只能是将事件函数定义内部的逻辑抽象。即,事件函数的处理逻辑是-每次+1,抽象为一个函数逻辑放在utils中。但是其他的公共部分:
1.定义state状态并初始化count值为0;(不能用utils抽象)
2.事件函数的定义(不能用utils抽象)
3.定义事件函数内填写处理逻辑:处理逻辑是-每次+1 (可以用utils抽象)
这里的1,2两点就属于组件逻辑,那么如果要做到最大程度的复用,就必须解决组件逻辑的复用。
3.2 this的不确定性
3.3 后期维护性困难
因为class不易拆分,很难拆分和重构。从设计模式来看,相比之下,函数式编程风格更适合前端代码开发。
4. “组件逻辑”复用的方案
如何评估一个问题对应的解决方案好坏(两个方面)
1.该方案是否彻底解决原有问题;
2.该方案本身是否带来新问题:
(1) 对原有体系的破坏性
(2) 方案自身问题 (上手难度,操作难易等)
(1) Minxin
| Mixin | 评估 |
|---|---|
| 方案采用方 | 【React】、【vue】 |
| 优点 | 解决了组件逻辑复用问题 |
| 缺点 | 1.对原有体系存在破坏性(变量覆盖,变量污染问题);2.当mixin数量过多时候,变量溯源困难 |
采用方:【React】、【vue】
优点:解决了组件逻辑复用问题
缺点:1.对原有体系存在破坏性(变量覆盖,变量污染问题); 2.当mixin数量过多时候,变量溯源困难
(2) 高阶组件
如何理解高阶组件
理解高阶组件文章:juejin.cn/post/740625…
问题:为什么vue不使用高阶组件?
vue本身也是可以使用高阶组件的,因为vue自身的DSL比react要重,如果要使用高阶组件的话会增加更多的复杂性和使用成本,因此vue中依然保持Mixin的复用方案。
| 高阶组件 | 评估 |
|---|---|
| 方案采用方 | 【React】 |
| 优点 | 解决了组件逻辑复用问题 |
| 缺点 |
(3) Hooks模式
总结
mixin模式
HOC模式
Hooks模式:
mixin && 高阶组件方式 && hooks 对比
从使用方式的外观表现上来看,组件使用mixin和hooks是使用什么就引用什么。而高阶组件则是对组件进行重新包装(外面嵌套一层父组件,这样才能将公共复用逻辑放在父组件中,以props传给子组件使用)。因此,高阶组件使用模式是类似洋葱模型那样包裹型的。而不是像hooks那样轻。
方案1.提供新的class-API来解决
方案2.另起炉灶增强函数组件
为什么要通过增强函数组件(即提供hooks)来替代类组件来解决问题?
原因1:其实相比于面向对象编程,函数式编程更符合前端的编程习惯:输入数据,render渲染,输出视图。
那么既然选择方案2代替,那么就要先将函数组件功能增强到能完全平替class组件,然后再解决class的缺点。
历史功能差距:生命周期、状态管理 解决class问题:逻辑服用 综上所述:在函数式组件中,通过hooks完成生命周期、状态管理、逻辑复用等几乎全部class组件开发工作的能力。
受到react影响,为什么vue也选择了hooks? juejin.cn/post/706695… vue的hooks和react的有什么区别?
二、Hooks代替class组件设计方案
2.1 抹平class功能差距
2.1.1 状态管理实现
通过使用useState,让函数组件可以管理视图数据
2.1.2 组件生命周期实现
组件的的生命周期主要分为四个大阶段:组件的创建、挂载、更新、卸载。 而我们日常开发中熟悉的各种生命周期钩子,也是围绕着这四个阶段来设置对应的钩子的。
(1) 类组件的生命周期
step1 - 挂载阶段(Mounting)
组件被实例化并插入 DOM 中的过程。这个阶段包括以下生命周期方法:
- 1-1
constructor()- 作用:初始化state,例如:this.state = { } - 1-2
static getDerivedStateFromProps()作用:获取props数据 - 1-3
render() - 1-4
componentDidMount()- 作用:执行初始化操作(比如发起网络请求、订阅事件、获取初始数据)
step2 - 更新阶段(Updating)
组件的 props 或 state 发生变化,导致组件重新渲染的过程。这个阶段包括以下生命周期方法:
- 2-1
static getDerivedStateFromProps()作用:获取props数据同挂载阶段的钩子 - 2-2
shouldComponentUpdate()- 作用:阻止子组件无效渲染,提升性能 - 2-3
render()- 作用:返回JSX模版 - 2-4
getSnapshotBeforeUpdate()- 作用:获取更新前的 DOM 快照或执行一些 DOM 操作,返回的值将作为componentDidUpdate方法的第三个参数传递。 - 2-5
componentDidUpdate()- 作用:拿到更新后的DOM
step3 - 卸载阶段(Unmounting)
组件从 DOM 中移除的过程。这个阶段只包含一个生命周期方法:
componentWillUnmount()- 作用:取消订阅操作
(2) 函数组件中的hooks实现
| 挂载阶段(4个) | Hooks 组件 |
|---|---|
| constructor | useState |
| getDerivedStateFromProps | useState里的update 函数 |
| render | 函数本身的render |
| componentDidMounted | useEffect |
| 更新阶段(5个) | Hooks 组件 |
|---|---|
| getDrivedStateFromProps | useState里的update 函数 |
| shouldComponentUpdate | useMemo |
| render | 函数本身的render |
| getSnapShotBeforeUpdate | -- |
| componentDidUpdate | useEffect |
| 卸载阶段(1个) | Hooks 组件 |
|---|---|
| componentWillUnmount | useLayoutEffect |
useEffect模拟生命周期
1.模拟componentDidMount: 组件首次挂载是欧
参考文献:
新旧生命周期对比:juejin.cn/post/728554…
对应到hooks:juejin.cn/post/684490…
举例:juejin.cn/post/703952…
2.1.3 class的pure-component实现
2.2 解决class存在问题
(1) 解决组件逻辑复用
三、Hooks具体原理解析
参见链接: