setState和useState的简单理解

667 阅读4分钟

useState是在函数组件中使用,setState在类组件中使用。

二者参数对比

setState:

构造函数是唯一可以给 state 赋值的地方

setState( updater,callback )

updater:object/function - 用于更新数据

callback:function - 用于获取更新后最新的 state 值

useState:

const [ state , setState ] = useState(initState)

initState:状态的初始值

state:value(默认值/当前值)

setState(updater) :改变value的方法

updater:object/function - 用于更新数据

什么数据写入 State

如何判断哪些数据是需要写入state的?

  1. 是否由父组件通过props传递进来的?
  2. 是否值永远都不会变化?
  3. 是否可以通过其他state或props计算得出?

如果有以上情况是肯定回答,那么这个值就可以不用写入state中。


State 的更新是异步还是同步

import React from "react";

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      countA: 0,
      countB: 0,
    };
  }

  handleClick = () => {
    setTimeout(() => {
      this.setState({ countA: this.state.countA + 10 });
      this.setState({ countA: this.state.countA + 10 }); // 执行两次
    }, 0);
  };

  handleClickAsync = () => {
    this.setState({ countB: this.state.countB + 10 });
    this.setState({ countB: this.state.countB + 10 }); // 两更新合并执行一次。
  };

  render() {
    console.log("render");
    console.log("countA: " + this.state.countA);
    // console.log("countB: " + this.state.countB);
    return (
      <div>
        <button onClick={this.handleClick}>同步执行{this.state.countA}</button>
        <button onClick={this.handleClickAsync}>
          异步执行{this.state.countB}
        </button>
      </div>
    );
  }
}

执行结果:

1、同步执行按钮:countA: 20,'render'打印两次

2、异步执行按钮:countB: 10,'render'打印一次

结论

只要进入了 react 的调度流程,那就是异步的,多次setState时进行批量更新处理。只要没有进入 react 的调度流程,那就是同步的。

什么时候不会进入 react 的调度流程呢? 在原生事件和 setTimeout、Promise.resolve().then 等情况下。

这些都不会走 React 的调度流程,在这种情况下调用 setState 就是同步的。 否则就是异步的。

而 setState 同步执行的情况下, DOM 也会被同步更新,也就意味着如果你多次 setState ,会导致多次更新,造成性能消耗。

注意

“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”。

scheduleUpdateOnFiber

setState 被调用后最终会走到 scheduleUpdateOnFiber 这个函数里面。

我们看看该函数中的这段代码:

executionContext 代表了目前 react 所处的阶段,而 NoContext 你可以理解为是 react 已经没内容执行的状态。而 flushSyncCallbackQueue 里面就会去同步调用我们的 this.setState ,也就是说会同步更新我们的 state 。所以,当 executionContext 等于 NoContext 的时候,我们的 setState 就是同步的。那什么地方会改变 executionContext 的值呢?

当 react 进入它自己的调度步骤时,会给这个 executionContext 赋予不同的值,表示不同的操作以及当前所处的状态,而 executionContext 的初始值就是 NoContext ,所以只要不进入 react 的调度流程,这个值就是 NoContext ,也就是说在setTimeout 、原生事件内调用 setState,那 setState 就是同步的更新。

拓展

flushSync, unstable_batchedUpdates

import React from "react";
import { flushSync, unstable_batchedUpdates } from "react-dom";

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      countA: 0,
      countB: 0,
    };
  }

  handleClick = () => {
    setTimeout(() => {
      unstable_batchedUpdates(() => {
        this.setState({ countA: this.state.countA + 10 });
        this.setState({ countA: this.state.countA + 10 });
      });
    });
  };

  handleClickAsync = () => {
    this.setState({ countB: this.state.countB + 10 });
    this.setState({ countB: this.state.countB + 10 });
  };

  render() {
    console.log("render");
    console.log(this.state.countA);
    // console.log(this.state.countB);
    return (
      <div>
        <button onClick={this.handleClick}>同步执行{this.state.countA}</button>
        <button onClick={this.handleClickAsync}>
          异步执行{this.state.countB}
        </button>
      </div>
    );
  }
}

通过上面两种方式处理,可以实现本该是同步更新的setState,变成异步的批量更新。

执行同步执行按钮时效果和异步执行按钮一样。

setState和useState更新引用数据类型比较

import React from "react";

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      countArray: [0, 1, 2, 3, 4],
    };
  }

  changeArray = () => {
    let array = this.state.countArray;
    array[0] = new Date().getTime();
    this.setState({
      countArray: array,
    });
  }

  render() {
    console.log("render");
    return (
      <div>
        <button onClick={this.changeArray}>
          修改数组{this.state.countArray[0]}
        </button>
      </div>
    );
  }
}

看上面第一段代码中的changeArray,array[0] = 时间戳,然后调用setState,看下结果,重新刷新,结果也变为当前时间戳。

useState同样是赋值后,再去setState,看下结果,却没有更新。

import React, { useState } from "react";

export default function Component() {
  const [countArray, setCountArray] = useState([0, 1, 2, 3]);

  const changeArray = () => {
    let array = countArray;
    // let array = countArray.splice(0, countArray.length);
    array[0] = new Date().getTime();
    setCountArray(array);
  };

  return (
    <div>
      <div>hooks</div>
      <button onClick={changeArray}>修改数组{countArray[0]}</button>
    </div>
  );
}

调试可以看到:这里判断了新setstate的数组和之前的数组比较,相同则return,不进行render。反之执行scheduleUpdateOnFiber函数。

React 使用 Object.is比较算法 来比较 state。如果相等则跳过渲染。

is函数源码

结语

react 本身已经做了很多优化措施,但是有时候也会因为代码不同的实现方式而导致 react 的性能优化失效,相当于我们自己做了反优化。所以理解 react 的运行原理对我们日常开发还是很有帮助的。 能力有限,如有不足请多指教。