React State

React State 相关知识

概念

state 是组件的状态,在组件的构造函数 constructor 中使用 this.state={}初始化,且 state 应是不可变对象,不可直接修改(直接修改不会触发组件的更新),应该通过 this.setState()来改变 state。

同步与异步

setState 是异步还是同步主要看是谁在调用它,大部分情况下是异步的,小部分情况是同步的(此说法适合 React18 之前,React 18+ setState 都是异步的)。

setState 的异步不是真的异步,setState 本身的执行过程是同步的,是 React 将多个 setState 进行合并和批量更新,导致其看起来像是异步的。

上图所示,当处于 batch update 的时候,会将组件更新推入 dirtyComponents 队列中等待执行,否则会立即执行 dirtyComponents 的队列更新,并且同步执行操作,不在进入异步队列。

异步的情况(batch update 为 true):

  1. 在 React 代理的合成事件中调用,如 onClick、onChange 事件,这些事件都是 React 为了代理原生事件而封装出来的一整套事件机制,我们称之为合成事件。

  2. 在钩子函数(生命周期)中调用(除 componentDidUpdate 外)

同步的情况(batch update 为 false):

  1. 在原生事件中调用,例如:

    class App extends React.Component{
        ...
    
        componentDidMount(){
            document.querySelector('#A').addEventListener('click',()=>{
                this.setState({
                    // 这里的 setState 是同步的
                })
            })
        }
    }
    
  2. setTimeout()等定时器内调用。

两个实例:

实例一:

请问四次输出的结果是什么?

class Test extends React.Component {
  state  = {
      count: 0
  };

  componentDidMount() {
    this.setState({count: this.state.count + 1});
    console.log(this.state.count);

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

    setTimeout(() => {
      console.log(this.state.count);
      this.setState({count: this.state.count + 1});
      console.log(this.state.count);

      this.setState({count: this.state.count + 1});
      console.log(this.state.count);
    }, 0);
  }
 
  render() {
    return null;
  }
};

因为前两个 setState 都是异步的,所以打印出来的都是0,且每次的this.state.count都是 0 ,所以即使异步完成,最后的结果也是 1 。当碰到定时器这样的原生事件,会立即生效,所以最后打印出来的结果是0 0 1 2 3

实例二:

下面有三个按钮:default,by addEventListener 和 by setTimeout,按钮被点击时都会将 count 值加 1,只不过实现过程不一样。查看例子

default: 无任何额外操作,直接 setState;

by addEventListener:设置元素的事件监听来触发 setState;

by setTimeout:将 setState 用定时器进行包裹;

从打印的结果可以看到,在事件监听(原生事件)或定时器内调用时,setState 是同步的,且能立即获取到变化后的值。然而上述的规则在 Hook 组件中是不适用的,不论哪种方式都不能即时获取 state 修改结果。那 hooks 出现这种情况的原因是什么呢?来看两个例子

let timer = null;

export default function Demo1() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    timer = setInterval(() => {
      setCount(count);
      console.log(`demo1-${count}`);
    }, 1000);
    return () => {
      if (timer) clearInterval(timer);
    };
  });

  return null;
}

上述代码每次打印出来的都是 demo1-0

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

useEffect(() => {
  setTimeout(() => {
    console.log(`demo2-${count}`);
  }, 1000);
}, [count]);

return (
  <div>
    <p>You clicked {count} times</p>
    <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
);

点击后打印出来的结果是 demo2-1demo2-2demo2-3

得到一个结论,hooks 在每次重新渲染的时候都会产生一个快照,state 的值都指向这个快照(相关知识:Capture Value),闭包是出现这种情况的原因,看个例子就能理解了:

function foo(increment){
    let counter = 0;
    function child(){
       counter += increment;
       console.log(counter);
       const msg = `counter is ${counter}`;
       return function inner(){
           console.log(msg);
       }
    } 
    return child; 
}

const _child = foo(1);
const _inner = _child(); // 1
_child(); // 2
_child(); // 3
_inner(); // counter is 1

解决异步副作用

有两种方式:

  1. 使用 Ref 来存储需要实时变化的值(但是这样值的变化不会通知页面重新渲染);

  2. setState 是异步的,所以不要依赖当前的 state 去计算下一个 state,如果有这种累加需求的,可以使用 state 的函数参数:

    handleClick=()=>{
        this.setState((prevState,props)=>{
            return { counter: prevState.counter + 1 }
        })
    }
    

    hook 下的写法:

    setCounter(count=>{
        return count + 1
    })
    

    state 可以接受函数参数,函数的第一个参数 prevState 是上一次执行 setState 方法后的结果,第二个参数 props 是当前最新的 props。

    实际的使用场景:一个列表需要全选功能,现已经有选中单项的方法,在全选时循环调用单项的选中方法,然后往 state 中的 active 数组 push 被选中的项的id,如果没有按照上述的方式进行改造,获取的 active 一直都是原始的值,结果就是只有最后一次是生效的。

举个迷惑性的例子:

state = {
  count: 0
}

componentDidMount(){
  this.setState({
    count: this.state.count + 1
  }, () => {
    console.log(this.state.count)
  })
  this.setState({
    count: this.state.count + 1
  }, () => {
    console.log(this.state.count)
  })
}

最后打印的结果是1,1,因为 React 会将修改同一个属性的操作进行合并,只有最后一个操作会被执行。

state = {
  count: 0
}

componentDidMount(){
  this.setState(
    preState=> ({
      count:preState.count + 1
    }),()=>{
      console.log(this.state.count)
    })
  this.setState(
    preState=>({
      count:preState.count + 1
    }),()=>{
      console.log(this.state.count)
    })
}

如果使用函数则操作不会被合并,每一个操作都会被执行,所以这里最后 count 的值是 2,由于回调时最后执行,所以这里打印的结果是2,2

作者

BiteByte

发布于

2021-08-08

更新于

2024-01-11

许可协议