前言
在React开发中,类组件开发是具备状态的,可以使用this.setState
来更新状态,使组件重新渲染,但在函数组件中,是不具备状态,也没有实例的概念,调用组件不再是创建类的实例,而是把函数执行,产生一个私有上下文,且在函数组件中不涉及this
的处理,因此React提供了Hooks函数,可以使用Hooks函数来控制函数组件的状态更新,useState
就是其中最重要之一。
Hooks函数是只能在函数组件中使用的函数,函数组件的每一次渲染(或者是更新),都是把函数(重新)执行,产生一个全新的“私有上下文”,内部的代码也需要重新执行。
什么是 useState
useState
是 React 提供的一个 Hook,用于在函数组件中添加状态。通过 useState
,你可以为组件引入内部状态,使其能够追踪和响应状态的变化。
基本语法
useState
返回一个包含两个元素的数组,第一个元素是当前状态的值,第二个元素是一个用于更新状态的函数。
const [state, setState] = useState(initialState);
state
是当前状态的值。setState
是用于更新状态的函数。initialState
是状态的初始值,只在组件的初始渲染时生效。
基本用法
让我们通过一个简单的计数器示例来演示 useState
的基本用法:
import React, { useState } from 'react';
import ReactDOM from 'react-dom/client';
const root = ReactDOM.createRoot(document.getElementById('root'));
const Counter = () => {
const [count, setCount] = useState(0);
return (
<div>
<h2>React的Hooks函数useState</h2>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};
root.render(
<>
<Counter></Counter>
</>
);
在这个例子中,我们使用 useState
创建了一个名为 count
的状态,初始值为 0。每次点击按钮时,会触发 setCount
操作,之后整个函数组件都会重新执行一遍,最后会把 count
的数量更新为 1。。
useState实现原理
useState
的工作原理:
1、当你第一次调用 useState
时,React 会使用你提供的初始值来设置state
。
2、在后续的渲染中,useState
会返回当前的 state
。
3、当你调用 setState
函数时,React 会重新渲染组件,并使用你提供的新值来更新 state
。
根据以上工作原理,我们可以实现一个简单的 useState
:
const root = ReactDOM.createRoot(document.getElementById('root'));
let _state
function useState(initialValue) {
_state = _state === undefined ? initialValue : _state
function setState(newState) {
_state = newState
render()
}
return [_state, setState]
}
const Counter = () => {
const [count, setCount] = useState(0);
return (
<div>
<h2>React的Hooks函数useState</h2>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};
function render() {
root.render(
<>
<Counter></Counter>
</>
);
}
render()
当点击按钮后,状态可以跟着改变了,至此,我们已经实现了一个非常简易的useState
。
但目前的实现仍然存在个问题,当在组件中同时使用两个useState
:
const root = ReactDOM.createRoot(document.getElementById('root'));
let _state
function useState(initialValue) {
_state = _state === undefined ? initialValue : _state
function setState(newState) {
_state = newState
render()
}
return [_state, setState]
}
const Counter = () => {
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
return (
<div>
<h2>React的Hooks函数useState</h2>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>count Increment</button>
<p>Num: {num}</p>
<button onClick={() => setNum(num + 1)}>num Increment</button>
</div>
);
};
function render() {
root.render(
<>
<Counter></Counter>
</>
);
}
render()
运行上面的代码,会发现,不论点击哪个按钮 count、num
都会同时改变。这是由于,我们将所有数据都放在一个 _state
中,导致 state
冲突。
因此可以把_state
存储在一个数组中,如 _state = [0, 0]
,这样可以保证数据一一对应,再次修改useState
,如下:
const root = ReactDOM.createRoot(document.getElementById('root'));
let _states = []; // 存储所有 state 的数组
let _index = 0; // 当前正在处理的 state 的索引
function useState(initialValue) {
const currentIndex = _index; // 在函数闭包中保存当前 state 的索引
_states[currentIndex] = _states[currentIndex] === undefined ? initialValue : _states[currentIndex]; // 如果当前 state 未定义,则使用初始值
function setState(newState) {
_states[currentIndex] = newState; // 更新当前 state
render()
_index = 0; // 重置索引
}
_index++; // 在返回前,将索引加 1,以便下次调用 useState 时获取下一个 state
return [_states[currentIndex], setState]
}
const Counter = () => {
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
return (
<div>
<h2>React的Hooks函数useState</h2>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>count Increment</button>
<p>Num: {num}</p>
<button onClick={() => setNum(num + 1)}>num Increment</button>
</div>
);
};
function render() {
root.render(
<>
<Counter></Counter>
</>
);
}
render()
再次运行,两个 useState
之间就不会相互影响了。至此,我们已经实现了一个单个组件可用的 useState
。这只是一个非常简化的模拟,实际的 useState
实现要复杂得多,并且包含了许多优化和错误处理的代码。
在React中,如果有多个组件调用 useState
,那么就会在每个组件都创建一个 _state
和 _index
,那这么多的_state
和 _index
存放在哪里呢?那是存放在每个组件对应的虚拟 DOM 对象上的,而每个虚拟节点是 FiberNode``,_state
的真实名称应为 memorizedState
。
每个节点都会有一个对应的Fiber
对象,他的数据解构如下:
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag;
this.key = key;
this.elementType = null; // 就是ReactElement的`?typeof`
this.type = null; // 就是ReactElement的type
this.stateNode = null;
// Fiber
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.firstContextDependency = null;
// ...others
}
useState更新原理
运行下面代码:
const Counter = () => {
console.log('函数执行了')
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1)
setTimeout(() => {
console.log('我是2s后输出的;',count)
},2000)
}
return (
<div>
<h2>React的Hooks函数useState</h2>
<p>Count: {count}</p>
<button onClick={handleClick}>count Increment</button>
</div>
);
};
浏览器效果
当点击按钮触发handleClick
方法,执行setCount
后,count
改为了1,Counter
函数也重新执行了,但2s后在setTimeout
里面却输出了:0,还是上次的值。为什么会这样呢?
因为当前组件每次重新render都会重新执行 useState
,useState
每次执行都会返回一个新的count
,setTimeout
函数中输出的count
还是重新 render
之前的count
,因此输出0。
useState更新多状态
运行下面代码:
const Counter = () => {
const [state, setState] = useState({
count:10,
num:20
});
const handleClick = () =>{
setState({
count:100
})
}
return (
<div>
<h2>React的Hooks函数useState</h2>
<p>Count: {state.count}</p>
<p>Num: {state.num}</p>
<button onClick={handleClick}>改变状态</button>
</div>
);
};
当点击按钮触发handleClick
,执行setState
更新count
的值为100,Counter
重新执行后,发现页面num
的值丢失了。
修改handleClick
,把state
修改前的值都传入setState
:
const Counter = () => {
const [state, setState] = useState({
count:10,
num:20
});
const handleClick = () =>{
setState({
...state,
count:100
})
}
return (
<div>
<h2>React的Hooks函数useState</h2>
<p>Count: {state.count}</p>
<p>Num: {state.num}</p>
<button onClick={handleClick}>改变状态</button>
</div>
);
};
当点击按钮触发handleClick
,执行setState
更新count
的值为100,发现页面的num
可以正确显示了。
因此useState
不能像类组件的setState
函数一样,支持部分状态更新,所以当有多个状态需要更新时,执行多次useState
,把不同状态分开进行管理。
const Counter = () => {
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
const handleClick = () =>{
setCount(100)
}
return (
<div>
<h2>React的Hooks函数useState</h2>
<p>Count: {count}</p>
<p>Num: {num}</p>
<button onClick={handleClick}>改变状态</button>
</div>
);
};
useState异步操作
useState
和类组件中的setState
一样,每次更新状态值,也不是立即更新,而是利用了更新队列updater机制来处理。
- 遇到
setState
会立即将其放入到更新队列中,此时状态和视图还都未更新 - 当所有的代码操作结束,会刷新队列,也就是通知更新队列中的所有任务执行:把所有放入的
setState
合并在一起执行,只触发一次状态更新和视图更新
const Counter = () => {
console.log('Counter函数执行了')
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
const handleClick = () =>{
setCount(count + 1)
setNum(num + 1)
console.log(count,num)//0 0
}
return (
<div>
<h2>React的Hooks函数useState</h2>
<p>Count: {count}</p>
<p>Num: {num}</p>
<button onClick={handleClick}>改变状态</button>
</div>
);
};
点击按钮“改变状态”,触发handleClick
,执行setCount、setNum
之后直接log,这时log输出的两个值都是0。
因为:
1、执行setCount、setNum
时,会把所有的setXXX
操作放到更新队列里面,执行完所有操作之后才会一次清空更新队列,因此console.log
先执行。
2、当执行setCount、setNum
时,会重新执行Counter
,因此每次console.log
里面拿到的值结果都是上一次的。
useState同步操作
类组件是通过flushSync
设置同步操作,执行函数会立即执行包裹在函数内或清空更新队列中的setState
,就不会等到所有操作都完成了才去清空更新队列了。
//..
import { flushSync } from 'react-dom';
//...
const Counter = () => {
console.log('Counter函数执行了')
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
const handleClick = () =>{
flushSync(() =>{
setCount(count + 1)
setNum(num + 1)
})
console.log(count, num)//0 0
}
return (
<div>
<h2>React的Hooks函数useState</h2>
<p>Count: {count}</p>
<p>Num: {num}</p>
<button onClick={handleClick}>改变状态</button>
</div>
);
};
浏览器效果
上面点击按钮 ,触发handleClick
,遇到flushSync
,会立即刷新更新队列,执行包裹在内的setXXX
,然后马上重新执行Counter
函数。这理可以看出先执行的Counter
函数,然后 在执行的console.log(count, num)
,最后结果输出0 0。
因此useState
不管是异步更新队列的还是同步更新队列,在handleClick
内拿到的状态都是更新之前的值。因为在每次执行Counter
函数,都会形成一个新的作用域,类似一个新的闭包。
下面来看看这个代码,结果会是什么呢?
const Counter = () => {
console.log('Counter函数执行了')
const [count, setCount] = useState(0);
const handleClick = () =>{
for(let i = 0; i< 10 ;i ++){
setCount(count + 1)
}
}
return (
<div>
<h2>React的Hooks函数useState</h2>
<p>Count: {count}</p>
<button onClick={handleClick}>改变状态</button>
</div>
);
};
当点击按钮,触发handleClick
,执行for循环后count
最后的状态值是1。
因为:
1、for循环内执行了10次setCount
,然而10次setCount
都会添加到更新队列中,然后在其他事情都做完之后,批处理一次更新完毕所有队列中的数据和视图,因此Counter
函数只执行了一次。
2、handleClick
里面的所有count
都是在上一级闭包中拿到的都是0,因此批处理中10个setCount
都是将count
更新为1。
useState函数更新
函数式更新,在函数内可以接收一个当前的状态值参数,并返回一个更新后的值!
运行下面代码:
const Counter = () => {
const [count, setCount] = useState(0);
const handleClick = () =>{
setCount((preCount)=>{
return preCount + 1
})
}
return (
<div>
<h2>React的Hooks函数useState</h2>
<p>Count: {count}</p>
<button onClick={handleClick}>改变状态</button>
</div>
);
};
当点击按钮,触发handleClick
,给setCount
传递一个函数作为参数,这个函数的preCount
是当前状态值,可以根据这个参数做一些需要累积的状态。比如:
const Counter = () => {
const [count, setCount] = useState(0);
const handleClick = () =>{
for(let i = 0; i < 10; i++){
setCount(preCount=>{
return preCount + 1
})
}
}
return (
<div>
<h2>React的Hooks函数useState</h2>
<p>Count: {count}</p>
<button onClick={handleClick}>改变状态</button>
</div>
);
};
以上结果count
的状态值是10。
useState初始化参数是函数
如果在组件内,一些初始化状态的值需要通过复杂的业务或计算获得,可以传入一个函数,在函数中计算并返回初始的state
,此函数只在初始渲染时被调用。
在父组件给子组件传递属性作为子组件的初始值:
const Parent = () => {
const [count, setCount] = useState(100);
const handleClick = () =>{
setCount(preCount=>{
return preCount + 1
})
}
return (
<div>
<h2>React的Hooks函数useState</h2>
<p>Count: {count}</p>
<button onClick={handleClick}>改变状态</button>
<Child x={count} y={20}></Child>
</div>
);
};
子组件收到父组件传递的props
内的x、y,然后通过useState
内的函数初始化子组件的状态值:
function Child(props){
const [num, setNum] = useState(() => {
const { x, y} = props
return x + y
});
const handleClick = () =>{
setNum(num + 1)
}
return (
<div>
<h2>我是子组件</h2>
<div>子组件的Num:{num}</div>
<button onClick={handleClick}>子组件改变状态</button>
</div>
)
}
上面子组件不管怎么改变状态和渲染页面都不会在执行useState
内的函数。