基本使用

函数式组件通过useState来支持state

  1. const [state, setState] = useState(initialValue);

useState接收一个参数作为初始的state值,返回一个数组,分别是current state和update state的方法,更新状态的函数我们称为state update function,每次state update function被调用,React都会重新渲染组件更新state。

  1. import React, { useState } from 'react';
  2. import './style.css';
  3. const App = () => {
  4. const initCount = 0;
  5. const [count, setCount] = useState(initCount);
  6. // demo1
  7. const handleAdd = () => {
  8. setCount(count + 1);
  9. console.log(count);
  10. };
  11. console.log('测试setter异步', count);
  12. const handleInitail = () => {
  13. setCount(initCount);
  14. };
  15. return (
  16. <div>
  17. <h4>Demo1:基本使用</h4>
  18. {count}
  19. <button onClick={handleInitail}>init</button>
  20. <button onClick={handleAdd}>+1</button>
  21. </div>
  22. );
  23. };
  24. export default App;

image.png
可以看到,点击+1按钮, 紧随setCount后的console打印中没有立刻改变
这点,setter和类组件中的setState是一致的,都是异步更改状态的

以上示例仅仅是维护了一个简单的原始类型的状态 如果维护复杂的状态,请使用useReducer,那么什么时候用useState什么时候使用useReducer呢?请参见 https://www.robinwieruch.de/react-usereducer-vs-usestate

内部原理

v2-c3c5d82705399034e66c472828fe96ea_b.gif

与之前的纯函数式组件相比,我们引入了 useState 这个钩子,瞬间就打破了之前 UI = render(data) 的安静画面——函数组件居然可以从组件之外把状态和修改状态的函数“钩”过来!并且仔细看上面的动画,通过调用 Setter 函数,居然还可以直接触发组件的重渲染

结合上面的动画,我们可以得出一个重要的推论:每次渲染具有独立的状态值(毕竟每次渲染都是完全独立的嘛)。也就是说,每个函数中的 state 变量只是一个简单的常量,每次渲染时从钩子中获取到的常量,并没有附着数据绑定之类的神奇魔法。
这也就是老生常谈的 Capture Value 特性。

高级使用

异步更新

那么,对于异步更新state会怎样呢?

  1. import React, { useState } from 'react';
  2. import './style.css';
  3. const App = () => {
  4. const initCount = 0;
  5. const [count, setCount] = useState(initCount);
  6. const handleAdd = () => {
  7. for (let i = 0; i < 3; i++) {
  8. setCount(count + 1);
  9. // setCount((count) => count + 1);
  10. }
  11. // setTimeout(() => {
  12. // setCount(count + 1);
  13. // }, 3000);
  14. };
  15. const handleInitail = () => {
  16. setCount(initCount);
  17. };
  18. return (
  19. <div>
  20. <h4>Demo2:异步</h4>
  21. {count}
  22. <button onClick={handleInitail}>init</button>
  23. <button onClick={handleAdd}>+1</button>
  24. </div>
  25. );
  26. };
  27. export default App;


通过for循环来模拟异步,可以发现:
点击一次+1按钮,并没有按照预期count的值变成3.

我们同样可以通过setTimeout来模拟异步,在线运行,会发现:
每次点击button,状态会在1s后更新(-1或者+1),然而,尝试在1s内连续点击button,会发现setCount在一秒内操作的state总是同一个count。一秒内无论点击多少次按钮,state总会加(减)1一次。

解释:

  • 每次渲染相互独立,因此每次渲染时组件中的状态、事件处理函数等等都是独立的,或者说只属于所在的那一次渲染
  • 我们在 count 为 0 的时候触发了 handleAlertClick 函数,这个函数所记住的count 也为 0
  • 针对setTimeout模拟三秒种后,刚才函数的 setTimeout 结束,输出当时记住的结果:3;
    针对for循环模拟,只记住了当时的0,进而只加了1

这道理就像,你翻开十年前的日记本,虽然是现在翻开的,但记录的仍然是十年前的时光。或者说,日记本 Capture 了那一段美好的回忆。

类组件中,由于执行上setState没有在react正常的函数执行上下文上执行,而是setTimeout中执行的,批量更新条件被破坏。原理这里我就不讲了,所以可以直接获取到变化后的state。
在class状态中,通过一个实例化的class,去维护组件中的各种状态;

但是在function组件中,没有一个状态去保存这些信息,每一次函数上下文执行,所有变量,常量都重新声明,执行完毕,再被垃圾机制回收。所以如上,无论setTimeout执行多少次,都是在当前函数上下文执行,此时num = 0不会变,之后setNumber执行,函数组件重新执行之后,num才变化。
所以, 对于class组件,我们只需要实例化一次,实例中保存了组件的state等状态。对于每一次更新只需要调用render方法就可以。

为了解决和类组件行为保持一致,你可以给state update function 传递一个函数:
它的参数是之前的状态,返回的是新的状态

给setCount传递函数作为参数,可以保证每次调用setCount时都会改变state,请记得:如果你的状态依赖 previous state,记得传递function作为state update function的参数。

  1. setCount((count) => count + 1);

我们发现,此时一秒内多次点击button,状态总会多次更新

useRef也可以同样实现 参见 函数式组件类组件区别

复杂状态

useState与 class 组件中的 setState 方法有个非常不同的一点,useState 不会自动合并更新对象,而是直接替换它
以上demo都是简单类型的状态,我们使用复杂类型看下

  1. import React, { useState } from 'react';
  2. import './style.css';
  3. const App = () => {
  4. const initCount = 0;
  5. const [count, setCount] = useState(initCount);
  6. const [person, setPerson] = useState({ name: 'joy', age: 34 });
  7. const [colors, setColor] = useState(['red', 'blue']);
  8. const handleChangePerson = () => {
  9. setPerson({ name: 'guan' });
  10. // setPerson({ ...person, name: 'guan' });
  11. };
  12. const handleChangeColor = () => {
  13. setColor([...colors, 'balck']);
  14. };
  15. return (
  16. <div>
  17. <h4>Demo3:更改对象状态</h4>
  18. <button onClick={handleChangePerson}>
  19. 改变对象状态: {JSON.stringify(person)}
  20. </button>
  21. </div>
  22. );
  23. };
  24. export default App;

image.png
image.png

可以看到 第二次的useState仅仅更新了name,并没有合并,而是直接替换。
我们可以用函数式的 setState 结合展开运算符来达到合并更新对象的效果。

  1. setState(prevState => {
  2. // 也可以使用 Object.assign
  3. return {...prevState, ...updatedValues};
  4. });
  5. //class中 会合并list
  6. this.state = {
  7. userInfo:[],
  8. list :[]
  9. }
  10. this.setState({userInfo:})

数组同理。

性能优化

通过 setXxx 设置新值,但是如果新值和当前值完全一样,那么会引发React重新渲染吗?

通过React官方文档可以知道,当使用 setXxx 赋值时,Hook会使用Object.is()来对比当前值和新值,结果为true则不渲染,结果为false就会重新渲染。

  1. let str='a';
  2. Object.is(str,'a'); //true
  3. let str='18';
  4. Object.is(str,18); //str为String类型,18为Number类型,因此结果为false
  5. let obj={name:'a'};
  6. Object.is(obj,{name:'a'}); //false
  7. //虽然obj和{name:'a'}字面上相同,但是obj==={name:'a'}为false,并且在Object.is()运算下认为两者不是同一个对象
  8. //事实上他们确实不是同一个对象,他们各自占用了一份内存
  9. let obj={name:'a'};
  10. let a=obj;
  11. let b=obj;
  12. Object.is(a,b); //因为a和b都指向obj,因此结果为true

由上面测试可以看出:
1、对于简单类型的值,例如String、Number 新旧值一样的情况下是不会引起重新渲染的;
2、对于复杂类型的值,即使新旧值 “看上去是一样的” 也会引起重新渲染。除非新旧值指向同一个对象,或者可以说成新旧值分别是同一个对象的引用;

采用复杂类型的值不是不可以用,很多场景下都需要用到,但是请记得上面的测试结果。
为了可能存在的性能问题,如果可以,最好避免使用复杂类型的值。

重点总结

(1)不像 class 中的 this.setState ,Hook 更新 state 变量总是替换它而不是合并它;
(2)推荐使用多个 state 变量,而不是单个 state 变量,因为 state 的替换逻辑而不是合并逻辑,并且利于后续的相关 state 逻辑抽离;
(3)当useXX的状态没有变化的时候,React 将跳过子组件的渲染及 effect 的执行。(React 使用 Object.is 比较算法 来比较 state。)

参考

https://www.robinwieruch.de/react-usestate-hook
https://zhuanlan.zhihu.com/p/130299058