先来看一个最简单的例子:
import { Component } from 'react';
class TestReact2 extends Component {
constructor(props) {
super(props);
}
state = {
count:0
}
addCount = () => {
this.setState({count:this.state.count+1})
console.log('count:'+this.state.count)
}
render() {
const { count } = this.state
return (
<div onClick={this.addCount}>
{count}
</div>
)
}
}
export default TestReact2
我们可以看到页面渲染的值和实际值是不同的,也就是说,React中setState的执行是"异步"的,'批量’的,而不是"同步"的。
为什么会出现这种情况呢?
其实setState并不是一个异步方法,之所有会有这种异步方法的表现形式,归根到底还是React框架本身的性能机制导致的。
React对setState做了特殊优化:
React会将多个setState的调用合并为一个来执行,也就是说,当执行setState的时候,state中的数据并不会马上更新
但是并不是所有情况下setState的执行都是异步的,这里先总结一下:
看一下效果:
import { Component } from 'react';
class TestReact2 extends Component {
constructor(props) {
super(props);
}
state = {
count:0
}
componentDidMount(){
this.setState({count:this.state.count+1})
console.log('count:'+this.state.count)
}
render() {
const { count } = this.state
return (
<div>
{count}
</div>
)
}
}
export default TestReact2
结果如上图,还是异步的效果
在生命周期中使用了setTimeout试一下:
componentDidMount(){
setTimeout(()=>{
this.setState({count:this.state.count+1})
console.log('count:'+this.state.count)
},1000)
}
说明setTimeout中useState是同步的
换成原生事件试试:
import { Component } from 'react';
class TestReact2 extends Component {
constructor(props) {
super(props);
}
state = {
count:0
}
componentDidMount(){
document.getElementById("ele").addEventListener("click", this.addCount);
}
addCount = ()=>{
this.setState({count:this.state.count+1})
console.log('count:'+this.state.count)
}
render() {
const { count } = this.state
return (
<div id="ele">
{count}
</div>
)
}
}
export default TestReact2
结果同上,说明在原生事件中,useState是同步的。
剩下的可以自行验证,比如在Promise里,或者多次对一个值通过useState进行赋值,结果是否与结论一致。
但是很多时候,我们需要同步的获取到更新之后的数据,这里介绍几种方法:
(1)useEffect中监听数据
import React, { useEffect, useState } from 'react'
const TestReact = (props) => {
const [count, setCount] = useState(0)
useEffect(()=>{
setCount(3)
console.log('count'+count)
}, [count])
return (
<div>
{count}
</div>
)
}
export default TestReact
(2)setState异步方法
如果是class组件,可以用setState回调函数捕捉最新值。
this.setState({count:3},()=>{
console.log('count'+this.state.count)
})
知道这一点是在Function组件中使用hooks的关键。
先来看一个例子:
import { Component } from 'react';
class TestReact2 extends Component {
constructor(props) {
super(props);
}
state = {
count:0
}
addCount = ()=>{
this.setState({count:this.state.count+1})
//setTimeout确保拿到的是同步的值
setTimeout(()=>{
console.log('count:'+this.state.count)
},3000)
}
render() {
const { count } = this.state
return (
<div onClick={this.addCount}>
{count}
</div>
)
}
}
export default TestReact2
同样的代码,在Function组件中看看效果:
import React, { useEffect, useState } from 'react'
const TestReact = (props) => {
const [count, setCount] = useState(0)
const addCount = ()=>{
setCount(count=>count+1) //防止多次点击的效果被吃掉
setTimeout(()=>{
console.log('count:'+count)
},3000)
}
return (
<div onClick={addCount}>
{count}
</div>
)
}
export default TestReact
我们发现,同样的逻辑在Function组件和Class组件中最后的效果完全不一样。
理解一下下面这段话:
在Class组件中:
1.state是不可改变的,setState后一定会生成一个全新的state引用
2.但是Class组件通过this.state方式读取state,这导致每次代码执行都会拿到最新的state引用,所以快速点击三次的结果都是3
在Function组件中:
1.useState产生的数据也是不可改变的,通过数组第二个参数set一个新值后,原来的值在下次渲染时会形成一个新的引用
2.但是Function组件对state的读取并不是通过this.方式,这就使得每次setTimeout都读取了当时渲染闭包环境的数据,虽然最新的值跟着最新的渲染改变了,但在旧的渲染里,状态依然是旧值
注意,在Component组件的例子里,第一个闭包时count值为0,第二个闭包count值更新为1,所以打印出来的结果是0,1,2
再看一个Function组件和Class组件的例子
import React, { useEffect, useState } from 'react'
const TestReact = (props) => {
const [count, setCount] = useState(5)
useEffect(()=>{
setTimeout(()=>{
setCount(3)
console.log('count'+count)
},0)
},[])
return (
<div>
{count}
</div>
)
}
export default TestReact
Class组件来实现一遍:
import { Component } from 'react';
class TestReact2 extends Component {
constructor(props) {
super(props);
}
state = {
count:5
}
componentDidMount(){
setTimeout(()=>{
this.setState({count:3})
console.log('count:'+this.state.count)
},0)
}
render() {
const { count } = this.state
return (
<div>
{count}
</div>
)
}
}
export default TestReact2
其实上面的两个例子原理是共通的,这里主要还是想讲一下Function组件的Render过程:
Function组件是更彻底的状态驱动抽象,并没有生命周期的概念,只有一个状态,React负责同步到DOM。
Function组件每次Render都有自己的Props和State,可以这么说,每次Render的内容都会形成一个快照并保存下来,因此当状态变更然后Render时,就会形成N个Render状态,每个Render状态都有自己固定不变的Props和State。
因此在上面那个例子中,count的值开始为5并进行了Render,setTimeout将该值输出,而在setTimeout里对count赋值为3则进入到了下次的全新Render。结合上面闭包所讲的,其实每个闭包就相当于一次Render的上下文环境,在某次Render中的数据则为该Render时的数据。
如果想要实现一个页面数字每隔1秒增加1的效果,基本上都会想到用定时器setInterval来实现,看个用useEffect实现的例子。
import React, { useEffect, useState } from 'react'
const TestReact = (props) => {
const [count, setCount] = useState(5)
useEffect(()=>{
let timer = setInterval(()=>{
setCount(count=>count+1)
console.log('count:'+count)
},1000)
},[])
return (
<div>
{count}
</div>
)
}
export default TestReact
实际count的值并没有更新,页面刚刚初始化的时候就开始执行setInterval函数,本次Render的count就会一直是5,除非下次有操作修改了count值,下次的Render就会拿到上次Render的count最终值。
举个例子:
import React, { useEffect, useState } from 'react'
const TestReact = (props) => {
const [count, setCount] = useState(5)
let timer = ''
useEffect(()=>{
timer = setInterval(()=>{
setCount(count=>count+1)
console.log('count:'+count)
},1000)
},[])
const changeCount = ()=> {
clearInterval(timer)
console.log('count:'+count)
}
return (
<div onClick={changeCount}>
{count}
</div>
)
}
export default TestReact
注意:每次点击之后拿到的数据确实是上次Render的count的最终值,但是之后setInterval定时器又开始了。
说明一下这个过程:
在changeCount里虽然没有对count的值做变动,但是实际count的实际值相对于初始值5已经发生了变动,所以刷新了整个组件,导致setInterval再次运行。
那么这里就有两个疑问:
1.useState的更新数据难道不是局部更新,为什么要刷新整个页面?
2.到底什么情况下会进入下次Render?
这里看一下之前的文章react Render机制
根据之前的处理方式,如果想要在setInterval中拿到count更新的值,我们只需要在useEffect中监听count即可。
试试:
import React, { useEffect, useState } from 'react'
const TestReact = (props) => {
const [count, setCount] = useState(5)
useEffect(()=>{
let timer = setInterval(()=>{
setCount(count=>count+1)
console.log('count:'+count)
},1000)
},[count])
return (
<div>
{count}
</div>
)
}
export default TestReact
这是因为一旦count发生变化就会启动一个定时器,以最新的count值增加1,反复启动多个定时器刷新数据,定时器数量指数级增加,使得count值变动越来越快。
这里需要对定时器做处理:
import React, { useEffect, useState } from 'react'
const TestReact = (props) => {
const [count, setCount] = useState(5)
useEffect(()=>{
let timer = setInterval(()=>{
setCount(count=>count+1)
console.log('count:'+count)
},1000)
return () => {
clearInterval(timer)
}
},[count])
return (
<div>
{count}
</div>
)
}
export default TestReact
实际上useEffect监听数据,同样的功能我们可以用setTimeout实现。
import React, { useEffect, useState } from 'react'
const TestReact = (props) => {
const [count, setCount] = useState(5)
useEffect(()=>{
setTimeout(()=>{
setCount(count=>count+1)
console.log('count:'+count)
},1000)
},[count])
return (
<div>
{count}
</div>
)
}
export default TestReact
实现的效果跟上面的图一样。