前端小白学 React 框架(十)

87 阅读5分钟

setState

React为什么要setState

开发中我们并不能直接通过修改state的值来让界面发生更新,因为我们修改了state之后,希望React根据最新的State来重新演染界面,但是这种方式的修改React并不知道数据发生了变化,React并没有实现类似于Vue2中的Object.defineProperty或者Vue3中的Proxy的方式来监听数据的变化,我们必须通过setState来告知React数据已经发生了变化

🧐 疑惑: 在组件中并没有实现setState的方法,为什么可以调用呢?原因很简单,setState方法是从Component中继承过来的

react中setState源码:

/**
 *
 * @param {object|function} partialState Next partial state or function to
 *        produce next partial state to be merged with current state.
 * @param {?function} callback Called after state is updated.
 * @final
 * @protected
 */
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');
};

🎉 小彩蛋 - 同时附上createElement源码的362行开始。

部分代码如下:

const childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    // ...
    props.children = childArray;
  }

⭕️ 填坑:所以在插槽那里,如果传一个参数则children的类型是object,如果传多个那么则是数组类型。

setState的多种用法

其实setState中第一个参数是对象,源码中使用Object.assign将其与this.state中的合并后更改。

基本用法

this.setState({ message: 'hello react' });

传入回调函数

setState接受两个参数:

  • 第二个参数是一个回调函数,这个回调函数会在更新后会执行,格式如下: setState(partialState, callback)
this.setState((state, props) => {
  // 可以编写关于message的处理逻辑
  console.log('App: ', state, props);
  return {
    message: 'hello react'
  }
})

这么做的好处:

  1. 可以在回调函数中编写新的state的逻辑
  2. 当前的回调函数会将之前的state和props 传递进来,举例如下:

屏幕录制2023-05-20 22.13.46.gif

setState是异步

this.setState({ message: 'hello react' });
console.log(this.state.message);

然后观察控制台的打印结果:

截屏2023-05-20 22.18.12.png

可以发现页面上的内容更改了,但是打印的时候还是原来的值

在更新后立马拿到正确结果

this.setState({ message: 'hello react' }, () => {
  console.log('callback:', this.state.message);
});
console.log(this.state.message);

然后观察控制台的打印结果:

截屏2023-05-20 22.21.58.png

setState异步更新

根据上面的例子可见setState是异步的操作,我们并不能在执行完setState之后立马拿到最新的state的结果,那为什么setState设计为异步呢?setState设计为异步其实之前在GitHub上也有很多的讨论,React核心成员(Redux的作者) Dan Abramov也有对应的回复,有兴趣的同学可以参考一下这个IssueDan Abramov(昵称为gaearon)的回答。

对其回答做一个简单的总结:

  1. setState设计为异步,可以显著的提升性能,如果每次调用setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的,最好的办法应该是获取到多个更新,之后进行批量更新
import React, { Component } from 'react'

export class App extends Component {
  constructor() {
    super();

    this.state = {
      message: 'hello world',
      count: 0
    }
  }

  changeText() {
    this.setState({ message: 'hello react' }, () => {
      console.log('callback:', this.state.message);
    });
    console.log(this.state.message);
  }

  add() {
    this.setState({ count: this.state.count + 1 });
    this.setState({ count: this.state.count + 1 });
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    const { message, count } = this.state;
    return (
      <div>
        <h1>setState的详细使用</h1>
        <h2>{message}</h2>
        <button onClick={() => this.changeText()}>更改文本</button>
        <p>当前计数:{count}</p>
        <button onClick={() => this.add()}>+1</button>
      </div>
    )
  }
}

export default App

点击按钮后,count会变成1,这就是因为setState是异步更新导致的,如果是同步更新的话,那么应该显示3,因为同步更新的话,每次的this.state.count的值每次都会不一样然后看一下实际效果吧:

屏幕录制2023-05-20 22.37.32.gif

如果想要实现点击加3的效果,那么应该使用setState的第二种用法:

this.setState((state) => {
  return {
    count: state.count + 1 // 1
  }
});
this.setState((state) => {
  return {
    count: state.count + 1 // 2
  }
});
this.setState((state) => {
  return {
    count: state.count + 1 // 3
  }
});

因为这样每次更新时传递过来的state不是this.state的值,而是上一次更新后的最新值,因此state的值是最新的,所以每次更新都会在上一次的基础上加1,最终会加3。来看一下效果吧:

屏幕录制2023-05-20 22.44.25.gif

  1. 如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步,state和props不能保持致性,会在开发中产生很多的问题。以下是回答的原话:

This is because, in the model you proposed, this.state would be flushed immediately but this.props wouldn’t. And we can’t immediately flush this.props without re-rendering the parent, which means we would have to give up on batching (which, depending on the case, can degrade the performance very significantly).

There are also more subtle cases of how this can break, e.g. if you’re mixing data from props (not yet flushed) and state (proposed to be flushed immediately) to create a new state: #122 (comment). Refs present the same problem: #122 (comment).

setState一定是异步的吗?

  1. react 18之前,在setTimeout中、在原生的DOM事件中、使用this.setState更新都是同步的,举个例子:
// setTimeout
setTimeout(() => {
  this.setState({ message: 'hello react' });
  console.log(this.state.message);
}, 0);

// 原生的DOM事件
btn.addEventListener('click', () => {
  this.setState({ message: 'hello react' });
  console.log(this.state.message);
});
  1. react 18之前,前在组件生命周期React合成事件中,setState是异步的;
  2. react 18之后,默认所有的操作都被放到了批处理中(异步处理),详细信息可以查看Automatic batching for fewer renders in React 18。如果想在react 18之后同步拿到更新的结果可以使用flushSync函数,具体使用如下:
import React, { Component } from 'react';
import { flushSync } from "react-dom"; // 要导入该函数

export class App extends Component {
  // ...

  changeText() {
    flushSync(() => {
      this.setState({ message: 'hello react' });
    })
    console.log(this.state.message); // 只能在这里打印
  }

  render() {
    const { message, count } = this.state;
    return (
      <div>
        <h1>setState的详细使用</h1>
        <h2>{message}</h2>
        <button onClick={() => this.changeText()}>更改文本</button>
        <p>当前计数:{count}</p>
        <button onClick={() => this.add()}>+1</button>
      </div>
    )
  }
}

export default App

具体的效果如下:

截屏2023-05-20 23.13.05.png

写在最后

如果大家喜欢的话可以收藏本专栏,之后会慢慢更新,然后大家觉得不错可以点个赞或收藏一下 🌟。

博客内的项目源码在react-app分支,大家可以拷贝下来。