react hooks踩坑合集

react hooks踩坑合集

一 setState到底是同步的还是异步的

先来看一个最简单的例子:

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 hooks踩坑合集_第1张图片

我们可以看到页面渲染的值和实际值是不同的,也就是说,React中setState的执行是"异步"的,'批量’的,而不是"同步"的。
为什么会出现这种情况呢?
其实setState并不是一个异步方法,之所有会有这种异步方法的表现形式,归根到底还是React框架本身的性能机制导致的。
React对setState做了特殊优化:
React会将多个setState的调用合并为一个来执行,也就是说,当执行setState的时候,state中的数据并不会马上更新
但是并不是所有情况下setState的执行都是异步的,这里先总结一下:

  • 在setTimeout和原生事件里setState是同步的,因为原生事件不受React管控
  • 在合成事件或者生命周期里是异步的,且如果对同一个值进行多次setState,setState的批量更新策略会对其进行覆盖,取最后一次的执行。
  • 在宏任务和微任务中是同步的,因为它们也不受React管控

看一下效果:

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)
  }

结果:
react hooks踩坑合集_第2张图片

说明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)
 })

二 Funciton组件和Class组件到底有哪些不同

知道这一点是在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

连续快速点击3次之后,效果如图:
react hooks踩坑合集_第3张图片

同样的代码,在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

react hooks踩坑合集_第4张图片

我们发现,同样的逻辑在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

结果:
react hooks踩坑合集_第5张图片

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

结果:
react hooks踩坑合集_第6张图片

其实上面的两个例子原理是共通的,这里主要还是想讲一下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

看一下实际的效果:
react hooks踩坑合集_第7张图片

实际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

多次点击之后效果如图:
react hooks踩坑合集_第8张图片

注意:每次点击之后拿到的数据确实是上次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

看一下结果:
react hooks踩坑合集_第9张图片

这是因为一旦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

看一下效果:
react hooks踩坑合集_第10张图片

实际上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

实现的效果跟上面的图一样。

你可能感兴趣的:(React,react.js,javascript,前端)