重新认识 React 的 setState

面试被问起 React 的 setState 是同步还是异步执行,根据长久以来的使用经验理所当然地回答“异步”,结果被告知存在同步执行的情况。遂查看 React 文档,重新学习一下。

用法

setState(updater[, callback])

  • setState(stateChange[, callback])
  • setState((state, props) => stateChange[, callback]) (v0.13 开始支持)

updater 可以是对象或者函数。一般情况下,setState 的更新是异步的,updater 会被放入到队列中,不会立即更新 state,触发重新渲染。

console.log(this.state.quantity); // 1

this.setState({quantity: 2}, () => {
  console.log(this.state.quantity); // 2
});

console.log(this.state.quantity); // 1

如果基于原来的 state 生成新 state,建议传入函数,避免多次调用时的结果不符合预期。

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

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

执行顺序

setState -> render -> componentDidUpdate -> setState callback

componentDidMount() {
  console.log('componentDidMount', this.state);
      
  this.setState({
    foo: 'bar'
  }, () => {
    console.log('setState callback', this.state);
  });
}
    
componentDidUpdate(prevProps, prevState) {
  console.log('componentDidUpdate', this.state);
}

render() {
  console.log('render', this.state);
  return (
      <h1>LifecycleDemo</h1>
  );
}

多次调用 setState 的效果

(1)通常情况(在 React 的生命周期、合成事件处理函数中调用 setState)表现为异步

在 React 的生命周期函数中

componentDidMount() {
  this.setState({
    foo: 'bar',
    x: true
  });
  
  this.setState({
    foo: 'baz',
    y: true
  });
}
componentDidMount() {
  this.setState(() => ({
    foo: 'bar',
    x: true
  }));
  
  this.setState(() => ({
    foo: 'baz',
    y: true
  }));
}

传入函数和传入对象的结果一致,都是批量更新,只触发一次重新渲染。

在 React 的合成事件处理函数中

handleClick () {
  console.log('before setState x', this.state);
  this.setState({
    foo: 'bar',
    x: true
  });
  console.log('after setState x', this.state);
    
  console.log('before setState y', this.state);
  this.setState({
    foo: 'baz',
    y: true
  });
  console.log('after setState y', this.state);
}

render() {
  return (
    <button onClick={this.handleClick}>Click Me</button>
  );
}
  • render 次数:1
  • render 时的 state{foo: 'baz', x: true, y: true}
  • 执行到 render() 前,打印 this.state 始终不变

setState 异步执行,多次 setState 只会产生一次批量更新,触发一次重新渲染。

(2)其他情况(在addEventListener、定时器、Promise等异步回调函数中调用 setState)表现为同步

componentDidMount() {
  Promise.resolve().then(() => {
    console.log('before setState x', this.state);
    this.setState({
      foo: 'bar',
      x: true
    });
    console.log('after setState x', this.state);
    
    console.log('before setState y', this.state);
    this.setState({
      foo: 'baz',
      y: true
    });
    console.log('after setState y', this.state);
  });
}
componentDidMount() {
  setTimeout(() => {
    this.setState({
      foo: 'bar',
      x: true
    });

    this.setState({
      foo: 'baz',
      y: true
    });
  });
}
  • render 次数:2
  • 两次 render 的 state
    • {foo: 'bar', x: true}
    • {foo: 'baz', x: true, y: true}

setState 同步执行,setState 执行后会立即执行 render()componentDidUpdate(),再执行下一个 setState

(3)未来(默认都是批量更新,异步)

Currently (React 16 and earlier), only updates inside React event handlers are batched by default. There is an unstable API to force batching outside of event handlers for rare cases when you need it.

In future versions (probably React 17 and later), React will batch all updates by default so you won't have to think about this. As always, we will announce any changes about this on the React blog and in the release notes.

——摘录自 Dan Abramov 的回答

总结

平时更新 state 时都是手动合并 stateChange,生成最终的 nextState 后,再调用 setState 一次性更新,完美地避开了 setState 行为不一致的地方……从减少心智负担地角度来说,不要依赖 React 实现批量更新,而应该自己手动合并,只调用一次 setState,保证 setState 异步执行的一致性。

this.setState(stateChange1);
this.setState(stateChange2);
this.setState(stateChange3);
this.setState({
  ...stateChange1,
  ...stateChange2,
  ...stateChange3
});

相关链接

发邮件与我交流

© 2016 - 2024 Ke Qingrong