21-hooks-useState

184 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情

在类组件中,setState可以是同步也可以是异步,结合class-setState问题食用。进行hooks中useState同步/异步问题与类组件中setState进行对比。

正文

  1. 异步--合成函数中
 const [count,setCount] = useState(0)
  let handleClick=()=>{
                setCount(count+1)
                console.log(count,"count");// 0 异步
}
  return(
                <div>
                    <h1>{count}</h1>
                  <button onClick={()=>{handleClick()}}>click</button>    
                </div>
)

点击按钮时,页面count变为1,打印还是0,说明合成函数中的事件是异步。

  1. 批量更新---合成函数内
 const [count,setCount] = useState(0)
            let handleClick=()=>{
                setCount(count+1)
                setCount(count+2)
                setCount(count+3)
                console.log(count,"count");// 0 异步
            }
            console.log("render");

image.png

即使方法调用了三次,但是render只执行了一次。内部也会等a更新完再去执行render渲染a状态。所以,即使改变了三次a值,也只执行一次render。

2-1 依赖累加---合成函数内

 const [count1, setCount1] = useState(0)
   let handleClick = () => {
                setCount1(pre=>pre+1)
                setCount1(pre=>pre+1)
                setCount1(pre=>pre+1) //每次依赖上次累加之后的count
            }
            console.log("render");

image.png

  1. 批量更新---合成函数内
            const [count1, setCount1] = useState(0)
            const [count2, setCount2] = useState(0)
            const [count3, setCount3] = useState(0)
            let handleClick = () => {
                setCount1(count1 + 1)
                setCount2(count2 + 1)
                setCount3(count3 + 1)
                console.log(count1, "count1");// 0 异步
                console.log(count2, "count1");// 0 异步
                console.log(count3, "count1");// 0 异步
            }
            console.log("render");

image.png

即使方法执行了三次,但是render只执行了一次。内部也会等count1,count2,count3更新完再去执行render渲染状态。所以,即使改变了三次值,也只执行一次render。

  1. 不同点----settimeout内
  setTimeout(()=>{
                    setCount1(count1+1);
                    console.log(count1,"count1"); //0
                },0)

image.png

类函数组件中,定时器內部setState为同步,而hooks钩子函数中仍为异步

  1. 无批量更新--settimeout内
  setTimeout(() => {
                    setCount1(count1 + 1);
                    setCount2(count2 + 1)
                    setCount3(count3 + 1)
                }, 0)

在定时器內部,原本的批量更新也不会触发。但是在定时器外部会批量触发哦。

image.png

  1. 原生事件--异步
 let handleClick2=()=>{
                setCount1(count1 + 1);
                console.log(count1,"count1");
            }
            document.getElementById("button2"), addEventListener('click', handleClick2,false)

image.png

新--浅比较问题:

当使用setName去修改name的值时,第一次会触发render,第二次第三次由于修改的值相同,就不会触发render了。只一次render,这是react18中fiber数据结构內部机制自动优化。

但class组件中没有数据优化方案,改几次值,即使为同样的值,也会同样render几次。

在链表结构中setState为单项指向的链表,不可以被修改顺序,所以自然不可以将钩子函数用于if/while语句中,如果使用该语句则会导致setState执行顺序发生改变,与链表结构冲突。

关于多个setState被合并成一次渲染的问题。并不是说在最后一次更改之前的setState()就不被渲染了,此前的多个setState它其实还是被执行了,只不过是由于多个setState处于环形链表中內部的优化机制去判断了是初始值还是新渲染的值,从而得出最优的一项。才会让我们拿到这个链表中最新的一项。

补充拓展:

类组件中render性能解决方案:

场景:初始name值为xiaohua,点击更改为lily。


class App extends React.Component{
     //state
state={
            name:"xiaohua",
      }
      //方法
     handleName=(name)=>{
           this.setState({
            name
           })
        }
        
        //显示
    <h1>{this.state.name}</h1>
    <button onClick={()=>{this.handleName("lily")}}>name</button>
}

方案1: shouldComponentUpdate(nextProps,nextState)生命周期在render之前运行,判断render是否应该更新。

ps:触发更新的数据条件,內部state改变/外部传值了。

 shouldComponentUpdate(nextProps,nextState){
            if(this.state.name===nextState.name){
                return false
            }
            return true
        }

image.png 结果优化成功,当数据没有发生改变时,点击按钮不触发render。

官方文档用的是Object.is()去判断是否发生改变,是否应该触发render

image.png

ps:

object.is,==,===区别

 console.log(""==false);//true
console.log(""===false); //false
console.log(Object.is("",false)); //false

console.log(-0===+0); //true
console.log(Object.is(-0,+0)); //false

方案2 :利用PureComonent

 class App extends React.PureComponent{ //调用
        state={
            name:"xiaohua",
            age:18,
            sex:"boy"
        }
}

相当于是shouldComponentUpdate的效果,自动优化render是否执行。

方案3

更换场景,给子组件传值

image.png

class Child extends React.Component{
            render(){
                console.log("child render");
                return(
                    <div>
                      child:{this.props.count}    
                    </div>
                )
            }
        }
 <Child count={this.state.count} />

当点击按钮时,去改变name/age时,即使child组件的count没有变,但也会跟着重复render。导致性能浪费。

解决方法:

  1. shouldComponentUpdate(nextProps,nextState)去判断;

在子组件中写:

 shouldComponentUpdate(nextProps,nextState){
                if(nextProps.count===this.props.count){
                    return false
                }
                return true
            }
  1. PrueComponent;
 class Child extends React.PureComponent{
 }

那如果换成是函数组件呢?

        function Child(props){
            console.log("child render");
            return  <div>
                      child:{props.count}    
                    </div>
        }

处理函数组件有一个专门的方法,react中的memo

 const MemoChild = React.memo(Child)
 //父组件中直接使用被包裹之后的memo组件
  <MemoChild count={this.state.count} />

效果与类组件中的PureComponent一样。达到render优化内部机制的效果。

续更:复杂数据类型

当useState中是复杂数据类型时又会是什么情呢?先设置为对象的形式。

let initStudent ={name:'lily',age:18}
const [student,setStudent] = useState(initStudent)

于是在页面中显示name和age两个数据,并设置一个按钮,点击的时候去改变这两个数据。

let handleClick =()=>{
                initStudent.name="xiaohua",
                initStudent.age=20
                setStudent(initStudent)
                console.log(initStudent);
            }

测试,是否修改成功呢?

image.png

失败,无论点几次,都没有render。

原因,在let initStudent初始会有一个地址,而setStudent时(initStudent)里面的地址与初始let时的地址相同,所以由于钩子函数内部机制去判断地址没有改变,所以没有修改页面。

解决方法:

将initStudent赋给一个新地址:

initStudent={name:'xiaohua',age:20}
setStudent(initStudent)

然后再去刷新页面,就可以看见render啦!

那如果初始是一个数组呢?是什么情况?

let  initStudent = [{name:'lily',age:18},{name:'lily1',age:18},{name:'lily2',age:18},]
const [students,setStudent] = useState(initStudent)

显示方法:

<div>
<ul>
{
students.map(student=>{
return<li key={student.name}>{student.name}---{student.age}</li> })
}
<button onClick={()=>{handleClick()}}>按钮</button>
</ul>       
</div>

当点击按钮时,向数组中push一个对象,并调用setStudent方法,查看结果:

let handleClick=()=>{
                initStudent.push({name:"lily:4",age:18})
                setStudent(initStudent)
                console.log(initStudent);
            }

结果一样,没有render

image.png

也是地址相同的问题,触发了钩子函数的隐藏技能,使其自动优化,于是还把它赋给新地址,来改变数组。

 initStudent=[...initStudent,{name:"lily:4",age:18}] //新地址
setStudent(initStudent)

解决啦。

要注意复杂数据类型在改变之前与之后的地址是否相同,如果相同的话会导致改变数据失败,要更改地址之后才能成功设置数据。