React Hooks 是 React 16.8 中引入的新特性,它允许你在不编写类组件的情况下使用状态和其他 React 特性。Hooks 在函数组件中使用,使得函数组件更加强大和灵活。

useState

useState 是 React 提供的一个 Hook,允许开发者在函数组件中添加状态。在 React Hooks 出现之前,状态只能在类组件中使用。useState 为函数组件提供了类似的功能,让开发者能够在组件中存储和更新值。

useState使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* @Description:
* @Author: xiuji
* @Date: 2024-01-19 09:34:57
* @LastEditTime: 2024-01-19 09:44:38
* @LastEditors: Do not edit
*/
import { useState } from "react";
const Demo = () => {
let [count, setCount] = useState(0);

return (
<div>
<h1>函数组件</h1>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
)
}

export default Demo;

useState函数式更新

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
/*
* @Description:
* @Author: xiuji
* @Date: 2024-01-19 09:34:57
* @LastEditTime: 2024-01-22 10:35:06
* @LastEditors: Do not edit
*/
import { useState } from "react";
const Demo = () => {
console.log('Demo render');
const [count, setCount] = useState(0);

const increment = () => {
// 使用函数式更新,这个函数接收当前的状态值,并返回一个更新后的值
for (let i = 0; i < 10; i++) {
setCount(prevCount => prevCount + 1);
}
};

return (
<div>
<p>{count}</p>
<button onClick={increment}>
点击
</button>
</div>
);
}

export default Demo;

在上述代码示例中,increment 函数在一个循环内连续调用 setCount 函数十次,使用了函数式更新来增加 count。使用函数式更新在这种情况下有几个优点:

  1. 确保状态更新的正确性
    函数式更新确保每次更新都基于最新的状态值。当状态更新依赖于之前的状态时,而这些状态更新可能会被批量处理或者异步执行,直接使用一个静态值可能会导致错误的结果。在示例中,即使 React 将这些 setCount 调用合并在一起,每一次函数调用仍然确保增量基于上一次的结果,这样可以正确地将 count 增加 10。

  2. 避免闭包问题
    在 React 的事件处理和异步操作中,如果直接引用状态值,可能会引用到旧的状态值,因为这些函数可能通过闭包捕获了状态值得旧的引用。函数式更新通过接受一个函数,这个函数的参数是当前的状态值,从而避免使用旧的状态值。

  3. 性能优化
    当多个状态更新可能在短时间内发生时,函数式更新能避免不必要的中间渲染。React 可以将多个 setCount 调用合并成一个状态更新,减少组件重渲染的次数。

  4. 代码简洁和可读性
    使用函数式更新可以使得状态更新的逻辑更加清晰,特别是当状态的下一个值依赖于前一个值时。这样的代码更易读,也更容易维护。

总的来说,函数式更新是处理复杂更新逻辑的强大工具,尤其是在有多个状态更新操作时。在示例中,即使 for 循环快速连续执行,每次调用 setCount 都会确保基于前一次更新后的状态值,从而能够正确地计算出新的状态。

useState性能优化

状态值比较

1
2
3
4
const [value, setValue] = useState(0);

// 这个更新不会引起组件的重新渲染,因为状态值没有变化
setValue(0);

useState中,如果新的状态值严格等于(使用===比较)旧的状态值,React则认为状态没有变化,因此不会触发组件的重新渲染。

这种优化有助于避免不必要的渲染,进而可以改善应用的性能。然而,需要注意的是,这种优化仅适用于简单值(如字符串、数字、布尔值)的比较,对于对象或数组这样的复杂数据结构,即使内容相同,但是引用地址不同,React 仍然会进行重新渲染。因此,对于复杂数据类型的状态管理,需要谨慎处理状态更新以避免不必要的渲染,例如使用不变性原则、React.memouseCallback等优化手段。

复杂逻辑使用函数式更新处理

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
/*
* @Description:
* @Author: xiuji
* @Date: 2024-01-19 09:34:57
* @LastEditTime: 2024-01-22 11:01:12
* @LastEditors: Do not edit
*/
import { useState } from "react";
const Demo = (props) => {
// 重新执行Demo时代码逻辑从上至下也会重新执行,复杂逻辑会造成性能浪费
const { A, B } = props;
let total = 0; // total只用作state的初始状态,后续状态修改后(执行setCount后)total的值不再有实际意义,但状态更新会导致获取total的逻辑重复执行
for (let i = A; i <= B; i++) {
console.log('====================================渲染执行');
total = +Math.random().toString().substring(2);
}
const [count, setCount] = useState(total);

const increment = () => {
setCount(99999); // 更新状态会重新执行函数组件Demo
};

return (
<div>
<p>{count}</p>
<button onClick={increment}>
点击
</button>
</div>
);
}

export default Demo;

上述代码for 循环在每次组件渲染时都会被执行。因为 useState 的初始值只在组件的首次渲染时使用,所以在后续的每次渲染过程中,这段逻辑都是不必要的,它消耗计算资源并可能导致性能下降。

借助函数式更新优化:

1
2
3
4
5
6
7
8
const [count, setCount] = useState(() => {
const { A, B } = props;
let total = 0;
for (let i = A; i <= B; i++) {
total = +Math.random().toString().substring(2);
}
return total;
});

这样优化是可行的,并且带来了几个优势:

  1. 性能优化
    通过传递一个初始化函数给 useState,这段函数只会在组件的初始渲染时执行一次来计算初始状态值。在后续的渲染中,即使组件的props发生变化,这个函数也不会被重新执行,从而节省了不必要的计算资源。

  2. 避免不必要的渲染
    此前的代码中,在组件每次渲染时都会执行计算 total 的代码,无论状态是否需要更新。现在,因为初始化逻辑被移入 useState 的懒初始化函数中,这些计算只在组件首次渲染时发生,不会在每次更新时重新计算。

  3. 更干净的组件逻辑
    初始化状态的逻辑现在被封装在 useState 中,这使得组件的主体部分更加清晰和易于维护。相关的初始化逻辑被集中在一处,不会散布在组件的其他部分。

useState底层处理机制

初始渲染

  1. 状态初始化:当组件首次渲染时,useState 接受的参数将作为状态的初始值。React 内部为每个状态创建了一个对应的“槽位”,用于在渲染间跟踪这些状态。

  2. Hook 链表:React 为每个组件维护了一个 Hooks 链表,每个 useState 调用在这个链表中占据一个位置。

  3. 返回值useState 返回一个状态值和一个可以更新该状态的函数。这个更新函数被绑定到对应的 Hook 和组件实例。

更新过程

  1. 触发更新:调用 useState 返回的更新函数会将新的状态值入队,并触发组件的重新渲染。

  2. 重新渲染:在组件的下一次渲染中,useState 不再使用初始值,而是通过组件的唯一标识从 React 的内部状态存储中检索已存储的最新状态值。

  3. 状态保持:由于 React 按照 useState 调用的顺序来维护状态,每次渲染都会按照同样的顺序读取或更新状态,因此,即使组件重新执行,状态也能得到正确的保持。

  4. 渲染优化:如果更新的状态值与当前的状态值相同,React 可以跳过该组件的重新渲染。

更新函数的实现

  1. 不变性:更新函数内部并不直接修改当前的状态,而是创建一个新的状态值。这与不可变数据模式相符,有助于优化渲染性能、避免副作用,并简化状态的比较逻辑。

  2. 异步更新:状态更新函数通常是异步的,这意味着调用状态更新函数后,状态的改变并不会立即反映。React 会批量处理状态更新,以提高性能。

  3. 调度优先级:React 可以根据不同的更新类型分配不同的优先级。例如,由于用户交互引起的更新可能会比数据获取引起的更新有更高的优先级。

注意事项

  • 调用顺序useState 需要在组件的顶层调用,不能在循环、条件或嵌套函数中调用,以确保状态的顺序和数量在每次渲染时都保持一致。
  • 闭包问题:由于 useState 更新函数可能是异步的,要注意闭包陷阱,即在更新函数内部直接引用旧的状态可能会导致引用了过期的状态值。

以上是对 useState 底层处理机制的一个高层次的概述。React 核心团队对其内部实现进行了高度优化,以确保性能和可靠性,而不需要开发者关注这些底层细节。开发者只需按照 React 的 Hooks API 规范编写代码即可。

函数组件状态/props更新时渲染逻辑

可以看到:

  • 每次状态或props更新时,组件函数重新执行,而且是一个全新的渲染上下文。
  • useState 在首次渲染时设置初始值,后续渲染则提供当前状态值。
  • useState 返回的状态设置函数会在每次渲染时被重新创建,但是其内部机制确保了状态值的更新和获取是正确的。
  • useEffect 可以执行副作用,并且它的执行依赖于传递给它的依赖数组。
  • 当组件卸载时,所有的清理工作(如useEffect中返回的函数)会被执行。

React的Hooks机制确保了即使在组件函数多次执行的情况下,这些状态和副作用仍能被正确地管理和维护。

PS:在React18中useState是异步的,在react16中useState放在合成事件/周期函数中是异步操作,但是放在其他的异步操作中(例如:定时器、手动的事件绑定等)它是同步的。