React State
React State 相关知识
概念
state 是组件的状态,在组件的构造函数 constructor 中使用 this.state={}
初始化,且 state 应是不可变对象,不可直接修改(直接修改不会触发组件的更新),应该通过 this.setState()
来改变 state。
同步与异步
setState 是异步还是同步主要看是谁在调用它,大部分情况下是异步的,小部分情况是同步的(此说法适合 React18 之前,React 18+ setState 都是异步的)。
setState 的异步不是真的异步,setState 本身的执行过程是同步的,是 React 将多个 setState 进行合并和批量更新,导致其看起来像是异步的。
上图所示,当处于 batch update 的时候,会将组件更新推入 dirtyComponents 队列中等待执行,否则会立即执行 dirtyComponents 的队列更新,并且同步执行操作,不在进入异步队列。
异步的情况(batch update 为 true):
在 React 代理的合成事件中调用,如 onClick、onChange 事件,这些事件都是 React 为了代理原生事件而封装出来的一整套事件机制,我们称之为合成事件。
在钩子函数(生命周期)中调用(除 componentDidUpdate 外)
同步的情况(batch update 为 false):
在原生事件中调用,例如:
class App extends React.Component{ ... componentDidMount(){ document.querySelector('#A').addEventListener('click',()=>{ this.setState({ // 这里的 setState 是同步的 }) }) } }
在
setTimeout()
等定时器内调用。
两个实例:
实例一:
请问四次输出的结果是什么?
class Test extends React.Component {
state = {
count: 0
};
componentDidMount() {
this.setState({count: this.state.count + 1});
console.log(this.state.count);
this.setState({count: this.state.count + 1});
console.log(this.state.count);
setTimeout(() => {
console.log(this.state.count);
this.setState({count: this.state.count + 1});
console.log(this.state.count);
this.setState({count: this.state.count + 1});
console.log(this.state.count);
}, 0);
}
render() {
return null;
}
};
因为前两个 setState 都是异步的,所以打印出来的都是0
,且每次的this.state.count
都是 0 ,所以即使异步完成,最后的结果也是 1 。当碰到定时器这样的原生事件,会立即生效,所以最后打印出来的结果是0 0 1 2 3
。
实例二:
下面有三个按钮:default,by addEventListener 和 by setTimeout,按钮被点击时都会将 count 值加 1,只不过实现过程不一样。查看例子
default: 无任何额外操作,直接 setState;
by addEventListener:设置元素的事件监听来触发 setState;
by setTimeout:将 setState 用定时器进行包裹;
从打印的结果可以看到,在事件监听(原生事件)或定时器内调用时,setState 是同步的,且能立即获取到变化后的值。然而上述的规则在 Hook 组件中是不适用的,不论哪种方式都不能即时获取 state 修改结果。那 hooks 出现这种情况的原因是什么呢?来看两个例子
let timer = null;
export default function Demo1() {
const [count, setCount] = useState(0);
useEffect(() => {
timer = setInterval(() => {
setCount(count);
console.log(`demo1-${count}`);
}, 1000);
return () => {
if (timer) clearInterval(timer);
};
});
return null;
}
上述代码每次打印出来的都是 demo1-0
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
console.log(`demo2-${count}`);
}, 1000);
}, [count]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
点击后打印出来的结果是 demo2-1
、 demo2-2
、 demo2-3
得到一个结论,hooks 在每次重新渲染的时候都会产生一个快照,state 的值都指向这个快照(相关知识:Capture Value),闭包是出现这种情况的原因,看个例子就能理解了:
function foo(increment){
let counter = 0;
function child(){
counter += increment;
console.log(counter);
const msg = `counter is ${counter}`;
return function inner(){
console.log(msg);
}
}
return child;
}
const _child = foo(1);
const _inner = _child(); // 1
_child(); // 2
_child(); // 3
_inner(); // counter is 1
解决异步副作用
有两种方式:
使用 Ref 来存储需要实时变化的值(但是这样值的变化不会通知页面重新渲染);
setState 是异步的,所以不要依赖当前的 state 去计算下一个 state,如果有这种累加需求的,可以使用 state 的函数参数:
handleClick=()=>{ this.setState((prevState,props)=>{ return { counter: prevState.counter + 1 } }) }
hook 下的写法:
setCounter(count=>{ return count + 1 })
state 可以接受函数参数,函数的第一个参数 prevState 是上一次执行 setState 方法后的结果,第二个参数 props 是当前最新的 props。
实际的使用场景:一个列表需要全选功能,现已经有选中单项的方法,在全选时循环调用单项的选中方法,然后往 state 中的 active 数组 push 被选中的项的id,如果没有按照上述的方式进行改造,获取的 active 一直都是原始的值,结果就是只有最后一次是生效的。
举个迷惑性的例子:
state = {
count: 0
}
componentDidMount(){
this.setState({
count: this.state.count + 1
}, () => {
console.log(this.state.count)
})
this.setState({
count: this.state.count + 1
}, () => {
console.log(this.state.count)
})
}
最后打印的结果是1,1
,因为 React 会将修改同一个属性的操作进行合并,只有最后一个操作会被执行。
state = {
count: 0
}
componentDidMount(){
this.setState(
preState=> ({
count:preState.count + 1
}),()=>{
console.log(this.state.count)
})
this.setState(
preState=>({
count:preState.count + 1
}),()=>{
console.log(this.state.count)
})
}
如果使用函数则操作不会被合并,每一个操作都会被执行,所以这里最后 count 的值是 2,由于回调时最后执行,所以这里打印的结果是2,2
React State