参考原文:https://juejin.cn/post/6891577820821061646 和 https://zh-hans.reactjs.org/docs/hooks-rules.html#explanation
React Hooks的由来
在React Hooks出现之前,React组件大致分为三类:
函数组件:
- 没有生命周期,也会被更新并挂载,但是没有生命周期函数
- 没有this(组件实例)
- 没有内部状态(state)
轻量,如果组件没有涉及到内部状态,只是用来渲染数据,函数式组件性能优越
1 | function MyComponent(props) { |
类组件:
- 普通类组件(React.Component)
1
2
3
4
5
6// Component
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}- 纯类组件(React.PureComponent)
- 不能重写shouldComponentUpdate
- 基于shouldComponentUpdate做了一些优化,通过prop和state的浅比较来实现shouldComponentUpdate,即如果是引用类型的数据,只会比较是否同一个引用地址,而不具体不叫这个地址存放的数据是否一致
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70// Component
class Welcome extends React.PureComponent {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
- 高阶组件:
上面两个组件是将props转化成UI展示,而高阶组件是将组件转换为另一个组件
> 假设,我们只需要或者部分需要类组件的功能,比如状态管理,比如生命周期,那么我们就必须使用完整的类组件,那么有没有一种,我想使用性能更好的函数组件,但是也想同时使用类组件的一些特性呢,React Hooks就是基于此出现的,通过它,可以更好的在函数组件中使用React的特性。
### React Hooks的好处
1. 跨组件复用: 其实 render props / HOC 也是为了复用,相比于它们,Hooks 作为官方的底层 API,最为轻量,而且改造成本小,不会影响原来的组件层次结构和传说中的嵌套地狱;
2. 类定义更为复杂:
不同的生命周期会使逻辑变得分散且混乱,不易维护和管理;
时刻需要关注this的指向问题;
代码复用代价高,高阶组件的使用经常会使整个组件树变得臃肿;
3. 状态与UI隔离: 正是由于 Hooks 的特性,状态逻辑会变成更小的粒度,并且极容易被抽象成一个自定义 Hooks,组件中的状态和 UI 变得更为清晰和隔离。
### React Hooks的钩子
- useState: 用于定义组件的 State,对标到类组件中this.state的功能
- useEffect:通过依赖触发的钩子函数,常用于模拟类组件中的componentDidMount,componentDidUpdate,componentWillUnmount方法
- useContext: 获取 context 对象
- useReducer: 类似于 Redux 思想的实现,但其并不足以替代 Redux,可以理解成一个组件内部的 redux,并不是持久化存储,会随着组件被销毁而销毁;属于组件内部,各个组件是相互隔离的,单纯用它并无法共享数据;配合useContext的全局性,可以完成一个轻量级的Redux
- useCallback: 缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染,具有性能优化的效果;
- useMemo: 用于缓存传入的 props,避免依赖的组件每次都重新渲染;
- useRef: 获取组件的真实节点;
- useLayoutEffect:DOM更新同步钩子。用法与useEffect类似,只是区别于执行时间点的不同。useEffect属于异步执行,并不会等待 DOM 真正渲染后执行,而useLayoutEffect则会真正渲染后才触发;可以获取更新后的 state;
- 自定义钩子(useXxxxx): 基于 Hooks 可以引用其它 Hooks 这个特性,我们可以编写自定义钩子。
### React Hooks原理:useState
React Hooks是利用链表(Hook1 -> Hook2 -> Hook3 ...->... -> HookN)来实现的,但是我们可以使用Array来模拟userState的实现原理:
当调用useState时,会返回一个包含变量和函数的元组,且state的初始值就是外部对象调用useState(初始值)时传入的参数。
调用setState时,除了更新state,useState还重新渲染了UI。
所以,下面这段代码:
```javascript
function Counter() {
const [num, setNum] = useState(0);
return (
<div>
<div>num: {num}</div>
<button onClick={() => setNum(num + 1)}>加 1</button>
<button onClick={() => setNum(num - 1)}>减 1</button>
</div>
);
}
可以使用上述思路,进行初步模拟:
1 | function render() { |
PS:由上面的代码可以看到,Hooks的核心其实时闭包,Hooks返回的state和setState方法,其实在内部使用闭包实现的,这也是为什么Hooks用不好,会造成内存泄漏的原因。
上面的代码中,state是保存在一个全局变量中的,以此类推,多个状态,那么应该保存在一个全局的array中,具体过程如下:
第一次渲染时候,根据 useState 顺序,逐个声明 state 并且将其放入全局 Array 中。每次声明 state,都要将 cursor 增加 1。
更新 state,触发再次渲染的时候。cursor 被重置为 0。按照 useState 的声明顺序,依次拿出最新的 state 的值,视图更新。
完整的模拟过程如下:
1 | import React from "react"; |
React Hooks原理:useEffect
useEffect的作用可以简单的理解成,对标类组件中的componentDidMount,componentDidUpdate,componentWillUnmount方法的集合,它用来监听state或props变化的时候,需要执行的相应操作。
依然使用Array来模拟实现:
1 | const allDeps = []; |
为什么不能在循环和判断内使用React Hooks
react官方给出了详细的解释:explanation
1 | function Form() { |
那么 React 怎么知道哪个 state 对应哪个 useState?答案是 React 靠的是 Hook 调用的顺序。因为我们的示例中,Hook 的调用顺序在每次渲染中都是相同的,所以它能够正常工作:
1 | // ------------ |
只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。
- 条件判断中使用Hooks的问题
1 | // 🔴 在条件语句中使用 Hook 违反第一条规则 |
在第一次渲染中 name !== ‘’ 这个条件值为 true,所以我们会执行这个 Hook。但是下一次渲染时我们可能清空了表单,表达式值变为 false。此时的渲染会跳过该 Hook,Hook 的调用顺序发生了改变:
1 | useState('Mary') // 1. 读取变量名为 name 的 state(参数被忽略) |
React 不知道第二个 useState 的 Hook 应该返回什么。React 会以为在该组件中第二个 Hook 的调用像上次的渲染一样,对应的是 persistForm 的 effect,但并非如此。从这里开始,后面的 Hook 调用都被提前执行,导致 bug 的产生
这就是为什么 Hook 需要在我们组件的最顶层调用。如果我们想要有条件地执行一个 effect,可以将判断放到 Hook 的内部:
1 | useEffect(function persistForm() { |
由此可知,if条件会导致Hooks执行顺序出现问题,是一定不可以使用的。
- 循环中使用Hooks的问题
如果开发人员能够确保循环时合理的并且可以保证顺序(循环次数固定),理论上,是可以使用Hooks的,但是,循环不一定都合理,比如动态可变的数组循环。
- 函数中使用Hooks的问题
其实,仔细研究就会发现,React Hooks是可以在函数中调用Hooks的,这就是自定义Hooks
由此可知,React提出的不能在条件,循环,函数内使用,是为了避免不可知的顺序错乱,如果你确保顺序不变,可以禁用linter规则,但是,你确定你牛到你知道自己写代码真的是如何执行的么?你确定顺序真的不会变么?如果不敢100%确定,那就遵守React Hooks的规则吧