...

【跟着ChatGPT学React】setState使用起来的注意

setState

前言

在 React 中,setState 是用于更新类组件(Class Component) 状态(state)的核心方法。它可以触发组件重新渲染(re-render),使 UI 发生更新。

1 为什么要使用 setState?

因为要页面发生刷新,即使你修改了一个数据。但只要不调用 setState,就不会进行比较页面渲染。(除非类组件是用this.forceUpdate()

比如下面的这段代码,即使打印出来的this.state.counter已经变了,但是页面也不会重新渲染。

this.state.counter += 1;  绝对不可以直接在上面修改  

image-20220531001937714

只能使用 setState(),这样才会进行虚拟 dom 的比较,然后决定是否刷新渲染。

.....  
increment() {  
  this.setState({  
    counter: this.state.counter + 1 //   
  });  
}  
.....  

2 setState 从哪里来的

从上面的代码可以看出来,我们是直接使用this.setState()这个方法的,但是我们并没有定义啊。那么从哪里来的呢?

答案!继承自 Component!

官方源码 Component

官方源码 setState

// react/packages/react/index.js  
export {  
  __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,  
  act as unstable_act,  
  Children,  
  Component,  
  Fragment,  
  Profiler,  
  .....  

// react/packages/react/src/ReactBaseClasses.js  
Component.prototype.setState = function(partialState, callback) {  
  if (  
    typeof partialState !== 'object' &&  
    typeof partialState !== 'function' &&  
    partialState != null  
  ) {  
    throw new Error(  
      'setState(...): takes an object of state variables to update or a ' +  
        'function which returns an object of state variables.',  
    );  
  }  

  this.updater.enqueueSetState(this, partialState, callback, 'setState');  
};  

👆 然后你就可以看到setState()的本体了。

这里的同步和异步其实说的是 🆕 是否可以拿到更新的值

3 异步更新还是同步更新?

异步更新,是人家 react 的团队大佬说的。why is setState asynchronous

验证如下

import { Component } from 'react';  

class State extends Component {  
  constructor(props) {  
    super(props);  
    this.state = {  
      msg: '我是🐱',  
    };  
  }  

  // ⚠️ 重点看这段代码  
  handleMsg() {  
    this.setState({  
      msg: '我是小狗',  
    });  
    // 如果是同步的话,在上面改成我是小狗之后  
    // 因为打印的是小狗才对  
    // 但是点击之后出来的确实小猫  
    // 这说明并非是按照顺序同步执行的,而是异步执行的  
    console.log(this.state.msg); // 我是🐱  
  }  

  render() {  
    return (  
      <>  
        <h2>{this.state.msg}</h2>  
        <button  
          onClick={(e) => {  
            this.handleMsg();  
          }}  
        >  
          click  
        </button>  
      </>  
    );  
  }  
}  

export default State;  

为什么要用异步呢?大佬是这样解释的。

  • 如果每次调用 setState 都进行一次更新,那么意味着 render 函数会被频繁调用,界面重新渲染,这样效率是很低的最好的办法应该是获取到多个更新,之后进行批量更新
  • 如果同步更新了 state,但是还没有执行 render 函数,那么 state 和 props 不能保持同步.state 和 props 不能保持一致性,会在开发中产生很多的问题

关于上面这个 state 和 props 的问题,我写一个代码来说明一下。这里设计到父子组件问题

import { Component } from "react";  

function Statechind(props) {  
  return <p> {props.msg}</p>;  
}  

class State extends Component {  
 .........  
  // ⚠️ 重点看这段代码  
  handleMsg() {  
    this.setState({  
      msg: "我是小狗"  
    });  
    console.log(this.state.msg); // 我是🐱  
  }  

  render() {  
    return (  
      <>  
        <h2>{this.state.msg}</h2>  
        <Statechind msg={this.state.msg} />  
                    .........  
      </>  
    );  
  }  
}  

export default State;  

image-20220531005823778

那么如何拿到异步的结果呢?

方式 1️⃣ setState()回调函数

从 setState 源码里可以看到,这个方法接受 2 个参数。function(partialState, callback)

  • 参数 1 function || object
  • 参数 2 回调函数 → 当 setState 里的值更新之后,就会触发这个回调函数。利用这个,就可以拿到异步结果。

全部代码如下 ↓

import { Component } from 'react';  

class State extends Component {  
  constructor(props) {  
    super(props);  
    this.state = {  
      msg: 'Hello world',  
    };  
  }  

  handleMsg() {  
    this.setState(  
      {  
        msg: 'Hello REACT',  
      },  
      // 解决方法1️⃣ 回调函数  
      () => {  
        console.log(this.state.msg);  
      }  
    );  
  }  

  render() {  
    return (  
      <div>  
        {this.state.msg}  
        <button onClick={this.handleMsg.bind(this)}>click to change msg</button>  
      </div>  
    );  
  }  
}  

export default State;  

方式 2️⃣,生命周期函数 componentDidUpdate()

因为这个声明周期函数就是在render()之后,页面重新渲染之后才执行的。这样的话,就肯定能拿到。

import { Component } from 'react';  

class State extends Component {  
  constructor(props) {  
    super(props);  
    this.state = {  
      msg: 'Hello world',  
    };  
  }  

  handleMsg() {  
    this.setState({  
      msg: 'Hello REACT',  
    });  
  }  

  // 方式 2️⃣ 获取异步更新的state  
  componentDidUpdate() {  
    console.log(this.state.msg);  
  }  

  render() {  
    return (  
      <div>  
        {this.state.msg}  
        <button onClick={this.handleMsg.bind(this)}>click to change msg</button>  
      </div>  
    );  
  }  
}  

export default State;  

🤔 如果 2 个异步获取数据的同时执行,那么是谁先呢?

答案是先执行 componentDidUpdate() ,然后 setState(更新的setState,回调函数)

4 同步更新(18 之后都是异步了)

在某些情况下其实还是同步的!

下面这段代码需要我在看一下,不然貌似

1️⃣setTimeout()

import React, { Component } from 'react';  

export default class App extends Component {  
  constructor(props) {  
    super(props);  
    this.state = {  
      msg: 'hello world',  
    };  
  }  

  handleMsg = () => {  
    // 1️⃣ 这里使用定时器就是同步的  
    setTimeout(() => {  
      this.setState({  
        msg: 'HELLO REACT',  
      });  
      // setTimout的回调函数里输出  
      console.log(this.state.msg); // HELLO REACT  
    }, 10);  
  };  

⚠️ 关于这个 setTimeout 有争议的。18 之后 setState 无论在哪里都是异步的了。所以上面带代码在 React18 里面输出的还是 hello world。

2️⃣ 原生事件

//  组件挂载完  
componentDidMount() {  
  // 2️⃣ 渲染之后就使用原生的事件监听  
  document.getElementById('btn').addEventListener('click', () => {  
    this.setState({  
      msg: 'HELLO DOMAPI',  
    });  
    console.log(this.state.msg);  
  });  
  //  必须在原始的事件监听里面,直接放在componentDidMount里面是不会  
  this.setState({  
    msg: 'HELLO DOMAPI',  
  });  
  console.log(this.state.msg);  
}  

5 决定同步和异步的本质是?

其实看源码可以找到,react 内部,

同步更新

  • setTimeout()
  • 原生事件。

异步更新

  • React 合成事件(onClick 这种)
  • 生命周期

上面 2 种更新其实是不同的上下文!原生时间的上下文和 react 事件的上下文不同,通过上下文来判断优先等级。判断是同步,异步,批处理。

源码找不到了,反正写在了这里附近。getCurrentPriorityLevel()

6 数据的合并

这里说的是数据的合并,不是 setState 的合并。关于数据是否会被完全覆盖掉的问题。

使用的源码在这里react/packages/react-reconciler/src/ReactUpdateQueue.new.js

// react/packages/react-reconciler/src/ReactUpdateQueue.new.js  
return assign({}, prevState, partialState);  

这里是 Object.assign({}, this.state,{id:uuid99})MDN 的 Object.assign 说明

Object.assign(target, ...sources);  
Object.assign({}, this.state, { id: uuid99 });  
// 相当于是把后面的全部source,都拷贝到了target{}这里  

image-20220208233417430

验证代码

import React, { Component } from 'react';  

export default class App extends Component {  
  constructor(props) {  
    super(props);  
    this.state = {  
      id: 'uuid1',  
      msg: 'hello world',  
    };  
  }  

  handleMsg = () => {  
    this.setState({  
      //  这里会不会给完全覆盖掉,导致上面的msg不见,最后显示的只有id呢  
      // 答案是不会,这里只会先浅拷贝。然后  
      id: 'uuid99',  
    });  
  };  

  render() {  
    return (  
      <div>  
        <h2>{this.state.id}</h2>  
        <h2>{this.state.msg}</h2>  
        <button onClick={this.handleMsg}>click me</button>  
      </div>  
    );  
  }  
}  

但是,如果你的数据是一个对象,或者是数组,那么就是会被覆盖的!

为什么会被覆盖的问题的话,这个其实是这样的。

constructor(props) {  
  super(props);  
  this.state = {  
    prim: 'prim', // 简单数据  
    user: { msg: 'hello world', name: 'chin' },  
  };  
}  

changeText() {  
  this.setState({  
    user: { msg: 'hello react' },  
  });  
}  
// { user: { msg: 'hello react' } }  
// ⚠️ 最后的结果是没有chin的,因为这里Object.assign 本质是浅拷贝  

上面的 setState 就想当于这种感觉了。

const a = {  
  prim: 'prim', // 简单数据  
  user: { msg: 'hello world', name: 'chin' }, // 复杂类型数据  
};  

const b = {  
  user: { msg: 'hello react' },  
};  

const c = Object.assign({}, a, b);  
console.log(c);  
// { prim: 'prim', user: { msg: 'hello react' } }  

7 本身的合并

如果对于同一个数据使用多次setState()怎么样。

🤔 下面这段代码

increment = () => {  
  this.setState({  
    counter: this.state.counter + 1,  
  });  
  this.setState({  
    counter: this.state.counter + 2,  
  });  
  this.setState({  
    counter: this.state.counter + 3,  
  });  
};  

// 结果是+1还是+3还是+6  
// 结果是+3,因为前面的会被后面的给覆盖掉。  

setState() 这里的本质也是看源码,会有一个 do/while。所有的 setState 本身会被合并。

如果不想被合并呢?setState()是可以被接受一个函数的,这个函数里面可以接受上一次的值。源码

const nextState = payload.call(instance, prevState, nextProps);  

然后根据这个

increment = () => {  
  // 如果是一个函数,会进行累加操作。每一次都会用prevState的值,所以会有累加效果。  
  this.setState((prevState, props) => {  
    return {  
      counter: prevState.counter + 1,  
    };  
  });  
  this.setState((prevState, props) => {  
    return {  
      counter: prevState.counter + 2,  
    };  
  });  
  this.setState((prevState, props) => {  
    return {  
      counter: prevState.counter + 3,  
    };  
  });  
};  

根据这个就可以判断setState()什么时候需要一个 function,什么时候需要一个 object。

以上 🎉

ChatGPT

下面这些应该是基于最新的 18 说的。(2025.2.21)

1 setState 的基本用法

在 React 类组件 中,组件的状态存储在 this.state,要更新状态,不能直接修改 this.state,必须使用 setState()

✅ 基本示例

import React, { Component } from 'react';  

class Counter extends Component {  
  constructor(props) {  
    super(props);  
    this.state = { count: 0 };  
  }  

  handleIncrement = () => {  
    this.setState({ count: this.state.count + 1 });  
  };  

  render() {  
    return (  
      <div>  
        <p>当前计数: {this.state.count}</p>  
        <button onClick={this.handleIncrement}>增加</button>  
      </div>  
    );  
  }  
}  

export default Counter;  

📌 关键点

  • 不能使用 this.state.count++ 直接修改状态,否则不会触发重新渲染。

  • setState() 触发 render() 方法,更新 UI。

2 setState 是异步的

React 不会立即更新 state,而是批量合并多个 setState 调用,以优化性能。

❌ setState 不是立即更新

handleClick = () => {  
  console.log('Before setState:', this.state.count);  
  this.setState({ count: this.state.count + 1 });  
  console.log('After setState:', this.state.count); // 🚨 这里 count 还是旧的  
};  

🔍 可能的输出

Before setState: 0  
After setState: 0  (不会立即更新)  

✅ 解决方案:使用 setState 的回调

this.setState({ count: this.state.count + 1 }, () => {  
  console.log('Updated state:', this.state.count); // ✅ 这里才是最新值  
});  

3 setState 可能会合并多个更新

React 可能会合并多个 setState() 调用,所以不能直接使用 this.state,而要使用 prevState

🚨 错误的做法

handleClick = () => {  
  this.setState({ count: this.state.count + 1 });  
  this.setState({ count: this.state.count + 1 });  
  this.setState({ count: this.state.count + 1 });  
};  

🔹 React 可能会优化成

this.setState({ count: this.state.count + 1 }); // 只执行一次更新  

✅ 解决方案:使用 prevState

handleClick = () => {  
  this.setState((prevState) => ({ count: prevState.count + 1 }));  
  this.setState((prevState) => ({ count: prevState.count + 1 }));  
  this.setState((prevState) => ({ count: prevState.count + 1 }));  
};  

🔹 这样 count 能正确递增 3 次,而不会被合并。

4 setState 合并对象

setState 只会合并顶层对象,不会合并深层对象。

this.setState({ user: { name: 'Alice', age: 25 } });  
this.setState({ user: { age: 26 } }); // ❌ 这样会覆盖整个 `user` 对象  

✅ 正确做法(使用 ... 展开运算符):

this.setState((prevState) => ({  
  user: { ...prevState.user, age: 26 },  
}));  

5 setState 在函数组件中的替代方案

在函数组件中,不使用 setState,而是使用 useState Hook。

import React, { useState } from 'react';  

function Counter() {  
  const [count, setCount] = useState(0);  

  return <button onClick={() => setCount(count + 1)}>点击增加 {count}</button>;  
}  

export default Counter;  

✅ useState 不会合并对象,所以需要手动合并:

setUser((prevUser) => ({ ...prevUser, age: 26 }));  

6 setState 和批量更新

React 18 之后,setState 可能不会合并

useEffect(() => {  
  setCount((c) => c + 1);  
  setCount((c) => c + 1);  
});  

🔹 React 18 之后 count = 2(不再合并)。

需要触发组件更新时(修改 UI)。

状态依赖于当前 state 时(使用 prevState 方式)。

在 useEffect 或 onClick 事件中修改状态

总结

  • setState 是异步的,不会立即更新 this.state,但可以用回调 setState({}, () => {...}) 获取最新值。

  • 多个 setState 可能会被合并,但如果使用 prevState 方式就不会。

  • setState 只会合并顶层对象,深层对象需要手动合并 ...。

  • 类组件用 setState,函数组件用 useState

共有评论(0)

登陆即可评论哦