三、useState
1、基本使用:
const [state,setState]=useState(initValue);
- useState函数接收一个初始化参数initialState,其返回值用数组解构出两个参数:state和setState。
- 在初始化渲染期间,返回的状态 (
state
) 与传入的第一个参数 (initialState
) 值相同。 setState
函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。- 在后续的重新渲染中,
useState
返回的第一个值将始终是更新后最新的 state。
注意: React 会确保 setState
函数的标识是稳定的,并且不会在组件重新渲染时发生变化。这就是为什么可以安全地从 useEffect
或 useCallback
的依赖列表中省略 setState
。
2、两种更新方式
useState的更新state分为两种,直接更新和函数式更新。
- 直接更新
- 这个很好理解,直接传入新的state值即可。
- 这种情况适用于与返回的新值与旧值不存在依赖关系
setState(newState)
- 函数式更新
- 这个新的state的值需要用一个函数来返回
- preState总能拿到上一次更新成功之后的最新状态
setState((preState)=>{return nextState})
区别:
如果正常使用,这两种方式没啥区别,但是如果是异步更新的话,他们之间的差别就会体现出来了
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setTimeout(() => {
setCount(count + 1)
}, 3000);
}
function handleClickFn() {
setTimeout(() => {
setCount((prevCount) => {
return prevCount + 1
})
}, 3000);
}
return (
<>
Count: {count}
<button onClick={handleClick}>+</button>
<button onClick={handleClickFn}>+</button>
</>
);
}
当设置为异步更新,点击按钮延迟到3s之后去调用setCount函数,当快速点击按钮时,也就是说在3s多次去触发更新,但是只有一次生效,因为 count 的值是没有变化的。
而当使用函数式更新 state 的时候,这种问题就没有了,因为它可以获取之前的 state 值,也就是代码中的 prevCount 每次都是最新的值。
这是因为使用函数更新的时候,setCount函数将会被放在一个任务队列中,每一个setCount函数都可以拿到上一次更新成功后状态值。
注意:
- 如果你的更新函数的返回值完全与当前的state相同,则随后的渲染就会跳过。
- hooks中的useState与class中setState不同,
useState
不会自动合并更新对象,这里可以使用函数式的setState结合展开运算符达到合并的目的 useReducer
是另一种可选方案,它更适合用于管理包含多个子值的 state 对象
initValue是初始的state,可以是任意的数据类型,也可以是一个有返回值的回调函数(惰性化state)
initialState
参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
3、惰性初始化state
initialState
参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用:
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
4、跳过state更新
- 调用 State Hook 的更新函数并传入当前的 state 时,React 将跳过子组件的渲染及 effect 的执行。(React 使用
Object.is
比较算法 来比较 state。) - 需要注意的是,React 可能仍需要在跳过渲染前渲染该组件。
- 不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。
- 如果你在渲染期间执行了高开销的计算,则可以使用
useMemo
来进行优化。
5、useState的一些常见问题
当组件中出现多个状态的时候该怎么办?
- 组件中出现多个状态就意味着useState函数就会被多次调用。
- useState函数接收的参数并没有规定接收什么类型的参数,它只是被作为一个初始值。
- 我们之前在class组件中使用setState方法,是将现在的数据与之前的state数据进行合并,然后返回为一个新状态。
- 而hooks调用setState方法是将之前的state数据进行替换,返回一个新状态。
- 当然hooks也提供了useReducer()函数,以redux的方式来管理state.
**问题:**在开发中我们可以发现,无论useState()函数被调用了多少次,他们之前都是互相不影响,都是相互独立的。这一点很重要。那么这是如何做到的呢?
React是根据useState的调用顺序来执行的。在React的内部声明了一个数组,按照按照其useState()函数调用的顺序,来保存其数据。所以,一定要在函数的外层保存useState,不能在if else,for循环中,子函数中调用useState.
因为React共享一个保存数据的数组,那么也会对其他函数组件也有影响。Capture Value特性的产生是在于每一次 重新render 的时候,都是重新去执行函数组件了,对于之前已经执行过的函数组件,并不会做任何操作。
来实现一个简单的useState函数:
let memoizedState=[];//hooks存放在这个数组
let cursor=0;//当前 state 下标
function useState(initalValue){
memoizedState[cursor]=state[cursor]||initalValue;
const currentCursor=cursor;
function setState(newState){
memoizedState[currentState]=newState;
render();
}
return [memoizedState[cursor++],setState];
}
React是如何实现的?
React 中是通过类似单链表的形式来代替数组的。通过 next 按顺序串联所有的 hook。
type Hooks = {
memoizedState: any, // 指向当前渲染节点 Fiber
baseState: any, // 初始化 initialState, 已经每次 dispatch 之后 newState
baseUpdate: Update<any> | null,// 当前需要更新的 Update ,每次更新完之后,会赋值上一个 update,方便 react 在渲染错误的边缘,数据回溯
queue: UpdateQueue<any> | null,// UpdateQueue 通过
next: Hook | null, // link 到下一个 hooks,通过 next 串联每一 hooks
}
memoizedState,cursor 是存在哪里的?如何和每个函数组件一一对应的?
我们知道,react 会生成一棵组件树(或Fiber 单链表),树中每个节点对应了一个组件,hooks 的数据就作为组件的一个信息,存储在这些节点上,伴随组件一起出生,一起死亡。