参考原文:https://juejin.cn/post/6891577820821061646https://zh-hans.reactjs.org/docs/hooks-rules.html#explanation

React Hooks的由来

在React Hooks出现之前,React组件大致分为三类:

  • 函数组件:

    1. 没有生命周期,也会被更新并挂载,但是没有生命周期函数
    2. 没有this(组件实例)
    3. 没有内部状态(state)

轻量,如果组件没有涉及到内部状态,只是用来渲染数据,函数式组件性能优越

1
2
3
4
5
6
7
function MyComponent(props) {
return (
<button className="body" onClick={props.onClick}>
{props.value}
</button>
);
}
  • 类组件:

    • 普通类组件(React.Component)
    1
    2
    3
    4
    5
    6
    // Component
    class Welcome extends React.Component {
    render() {
    return <h1>Hello, {this.props.name}</h1>;
    }
    }
    • 纯类组件(React.PureComponent)
    1. 不能重写shouldComponentUpdate
    2. 基于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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function render() {
ReactDom.render(<Counter />, document.getElementById("roor"));
}

let state;

function useState(initialState) {
state = state || initialState;
function setState(newState) {
state = newState;
render();
}
return [state, setState];
}

// 首次渲染
render();

PS:由上面的代码可以看到,Hooks的核心其实时闭包,Hooks返回的state和setState方法,其实在内部使用闭包实现的,这也是为什么Hooks用不好,会造成内存泄漏的原因。

上面的代码中,state是保存在一个全局变量中的,以此类推,多个状态,那么应该保存在一个全局的array中,具体过程如下:

  • 第一次渲染时候,根据 useState 顺序,逐个声明 state 并且将其放入全局 Array 中。每次声明 state,都要将 cursor 增加 1。

  • 更新 state,触发再次渲染的时候。cursor 被重置为 0。按照 useState 的声明顺序,依次拿出最新的 state 的值,视图更新。

完整的模拟过程如下:

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
import React from "react";
import ReactDOM from "react-dom";

const states = [];
let cursor = 0;

function useState(initialState) {
const currenCursor = cursor;
states[currenCursor] = states[currenCursor] || initialState; // 检查是否渲染过

function setState(newState) {
states[currenCursor] = newState;
render();
}

cursor+=1; // 更新游标
return [states[currenCursor], setState];
}

function App() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(1);

return (
<div>
<div>count1: {count1}</div>
<div>
<button onClick={() => setCount1(count1 + 1)}>add1 1</button>
<button onClick={() => setCount1(count1 - 1)}>delete1 1</button>
</div>
<hr />
<div>num2: {num2}</div>
<div>
<button onClick={() => setCount2(count2 + 1)}>add2 1</button>
<button onClick={() => setCount2(count2 - 1)}>delete2 1</button>
</div>
</div>
);
}

function render() {
ReactDOM.render(<App />, document.getElementById("root"));
cursor = 0; // 重置cursor
}

// 首次渲染
render();

React Hooks原理:useEffect

useEffect的作用可以简单的理解成,对标类组件中的componentDidMount,componentDidUpdate,componentWillUnmount方法的集合,它用来监听state或props变化的时候,需要执行的相应操作。

依然使用Array来模拟实现:

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
const allDeps = [];
let effectCursor = 0;

function useEffect(callback, deps = []) {
if (!allDeps[effectCursor]) {
// 初次渲染:赋值 + 调用回调函数
allDeps[effectCursor] = deps;
effectCursor+=1;
callback();
return;
}

const currenEffectCursor = effectCursor;
const rawDeps = allDeps[currenEffectCursor];
// 检测依赖项是否发生变化,发生变化需要重新render
const isChanged = rawDeps.some(
(dep,index) => dep !== deps[index]
);
// 依赖变化
if (isChanged) {
// 执行回调
callback();
// 修改新的依赖
allDeps[effectCursor] = deps;
}
// 游标递增
effectCursor+=1;
}

function render() {
ReactDOM.render(<App />, document.getElementById("root"));
effectCursor = 0; // 注意将 effectCursor 重置为0
}

为什么不能在循环和判断内使用React Hooks

react官方给出了详细的解释:explanation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Form() {
// 1. Use the name state variable
const [name, setName] = useState('Mary');

// 2. Use an effect for persisting the form
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});

// 3. Use the surname state variable
const [surname, setSurname] = useState('Poppins');

// 4. Use an effect for updating the title
useEffect(function updateTitle() {
document.title = name + ' ' + surname;
});

// ...
}

那么 React 怎么知道哪个 state 对应哪个 useState?答案是 React 靠的是 Hook 调用的顺序。因为我们的示例中,Hook 的调用顺序在每次渲染中都是相同的,所以它能够正常工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ------------
// 首次渲染
// ------------
useState('Mary') // 1. 使用 'Mary' 初始化变量名为 name 的 state
useEffect(persistForm) // 2. 添加 effect 以保存 form 操作
useState('Poppins') // 3. 使用 'Poppins' 初始化变量名为 surname 的 state
useEffect(updateTitle) // 4. 添加 effect 以更新标题

// -------------
// 二次渲染
// -------------
useState('Mary') // 1. 读取变量名为 name 的 state(参数被忽略)
useEffect(persistForm) // 2. 替换保存 form 的 effect
useState('Poppins') // 3. 读取变量名为 surname 的 state(参数被忽略)
useEffect(updateTitle) // 4. 替换更新标题的 effect

// ...

只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。

  • 条件判断中使用Hooks的问题
1
2
3
4
5
6
// 🔴 在条件语句中使用 Hook 违反第一条规则
if (name !== '') {
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
}

在第一次渲染中 name !== ‘’ 这个条件值为 true,所以我们会执行这个 Hook。但是下一次渲染时我们可能清空了表单,表达式值变为 false。此时的渲染会跳过该 Hook,Hook 的调用顺序发生了改变:

1
2
3
4
useState('Mary')           // 1. 读取变量名为 name 的 state(参数被忽略)
// useEffect(persistForm) // 🔴 此 Hook 被忽略!
useState('Poppins') // 🔴 2 (之前为 3)。读取变量名为 surname 的 state 失败
useEffect(updateTitle) // 🔴 3 (之前为 4)。替换更新标题的 effect 失败

React 不知道第二个 useState 的 Hook 应该返回什么。React 会以为在该组件中第二个 Hook 的调用像上次的渲染一样,对应的是 persistForm 的 effect,但并非如此。从这里开始,后面的 Hook 调用都被提前执行,导致 bug 的产生

这就是为什么 Hook 需要在我们组件的最顶层调用。如果我们想要有条件地执行一个 effect,可以将判断放到 Hook 的内部:

1
2
3
4
5
6
useEffect(function persistForm() {
// 👍 将条件判断放置在 effect 中
if (name !== '') {
localStorage.setItem('formData', name);
}
});

由此可知,if条件会导致Hooks执行顺序出现问题,是一定不可以使用的。

  • 循环中使用Hooks的问题

如果开发人员能够确保循环时合理的并且可以保证顺序(循环次数固定),理论上,是可以使用Hooks的,但是,循环不一定都合理,比如动态可变的数组循环。

  • 函数中使用Hooks的问题

其实,仔细研究就会发现,React Hooks是可以在函数中调用Hooks的,这就是自定义Hooks

由此可知,React提出的不能在条件,循环,函数内使用,是为了避免不可知的顺序错乱,如果你确保顺序不变,可以禁用linter规则,但是,你确定你牛到你知道自己写代码真的是如何执行的么?你确定顺序真的不会变么?如果不敢100%确定,那就遵守React Hooks的规则吧