基本使用
函数式组件通过useState来支持state
const [state, setState] = useState(initialValue);
useState接收一个参数作为初始的state值,返回一个数组,分别是current state和update state的方法,更新状态的函数我们称为state update function,每次state update function被调用,React都会重新渲染组件更新state。
import React, { useState } from 'react';
import './style.css';
const App = () => {
const initCount = 0;
const [count, setCount] = useState(initCount);
// demo1
const handleAdd = () => {
setCount(count + 1);
console.log(count);
};
console.log('测试setter异步', count);
const handleInitail = () => {
setCount(initCount);
};
return (
<div>
<h4>Demo1:基本使用</h4>
{count}
<button onClick={handleInitail}>init</button>
<button onClick={handleAdd}>+1</button>
</div>
);
};
export default App;
可以看到,点击+1按钮, 紧随setCount后的console打印中没有立刻改变
这点,setter和类组件中的setState是一致的,都是异步更改状态的
以上示例仅仅是维护了一个简单的原始类型的状态 如果维护复杂的状态,请使用useReducer,那么什么时候用useState什么时候使用useReducer呢?请参见 https://www.robinwieruch.de/react-usereducer-vs-usestate
内部原理
与之前的纯函数式组件相比,我们引入了 useState 这个钩子,瞬间就打破了之前 UI = render(data) 的安静画面——函数组件居然可以从组件之外把状态和修改状态的函数“钩”过来!并且仔细看上面的动画,通过调用 Setter 函数,居然还可以直接触发组件的重渲染!
结合上面的动画,我们可以得出一个重要的推论:每次渲染具有独立的状态值(毕竟每次渲染都是完全独立的嘛)。也就是说,每个函数中的 state 变量只是一个简单的常量,每次渲染时从钩子中获取到的常量,并没有附着数据绑定之类的神奇魔法。
这也就是老生常谈的 Capture Value 特性。
高级使用
异步更新
那么,对于异步更新state会怎样呢?
import React, { useState } from 'react';
import './style.css';
const App = () => {
const initCount = 0;
const [count, setCount] = useState(initCount);
const handleAdd = () => {
for (let i = 0; i < 3; i++) {
setCount(count + 1);
// setCount((count) => count + 1);
}
// setTimeout(() => {
// setCount(count + 1);
// }, 3000);
};
const handleInitail = () => {
setCount(initCount);
};
return (
<div>
<h4>Demo2:异步</h4>
{count}
<button onClick={handleInitail}>init</button>
<button onClick={handleAdd}>+1</button>
</div>
);
};
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的参数。
setCount((count) => count + 1);
我们发现,此时一秒内多次点击button,状态总会多次更新
useRef也可以同样实现 参见 函数式组件类组件区别
复杂状态
useState与 class 组件中的 setState 方法有个非常不同的一点,useState 不会自动合并更新对象,而是直接替换它。
以上demo都是简单类型的状态,我们使用复杂类型看下
import React, { useState } from 'react';
import './style.css';
const App = () => {
const initCount = 0;
const [count, setCount] = useState(initCount);
const [person, setPerson] = useState({ name: 'joy', age: 34 });
const [colors, setColor] = useState(['red', 'blue']);
const handleChangePerson = () => {
setPerson({ name: 'guan' });
// setPerson({ ...person, name: 'guan' });
};
const handleChangeColor = () => {
setColor([...colors, 'balck']);
};
return (
<div>
<h4>Demo3:更改对象状态</h4>
<button onClick={handleChangePerson}>
改变对象状态: {JSON.stringify(person)}
</button>
</div>
);
};
export default App;
可以看到 第二次的useState仅仅更新了name,并没有合并,而是直接替换。
我们可以用函数式的 setState 结合展开运算符来达到合并更新对象的效果。
setState(prevState => {
// 也可以使用 Object.assign
return {...prevState, ...updatedValues};
});
//class中 会合并list
this.state = {
userInfo:[],
list :[]
}
this.setState({userInfo:})
数组同理。
性能优化
通过 setXxx 设置新值,但是如果新值和当前值完全一样,那么会引发React重新渲染吗?
通过React官方文档可以知道,当使用 setXxx 赋值时,Hook会使用Object.is()来对比当前值和新值,结果为true则不渲染,结果为false就会重新渲染。
let str='a';
Object.is(str,'a'); //true
let str='18';
Object.is(str,18); //str为String类型,18为Number类型,因此结果为false
let obj={name:'a'};
Object.is(obj,{name:'a'}); //false
//虽然obj和{name:'a'}字面上相同,但是obj==={name:'a'}为false,并且在Object.is()运算下认为两者不是同一个对象
//事实上他们确实不是同一个对象,他们各自占用了一份内存
let obj={name:'a'};
let a=obj;
let b=obj;
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