Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

setState 是异步还是同步更新 this.state #24

Open
iotale opened this issue Mar 5, 2021 · 0 comments
Open

setState 是异步还是同步更新 this.state #24

iotale opened this issue Mar 5, 2021 · 0 comments
Labels

Comments

@iotale
Copy link
Owner

iotale commented Mar 5, 2021

所谓同步还是异步指的是调用 setState 之后是否马上能得到最新的 state

setState 的异步

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

可以通过第二个参数 setState(partialState, callback) 中的 callback 拿到更新后的结果。:

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

一个具体例子:

handleClickOnLikeButton () {
  this.setState((prevState) => {
    return { count: 0 }
  })
  this.setState((prevState) => {
    return { count: prevState.count + 1 } // 上一个 setState 的返回是 count 为 0,当前返回 1
  })
  this.setState((prevState) => {
    return { count: prevState.count + 2 } // 上一个 setState 的返回是 count 为 1,当前返回 3
  })
  // 最后的结果是 this.state.count 为 3
}

先说结论:既可以异步更新也可以同步更新

  • 异步:

    • 合成事件的回调中或在生命周期函数中直接调用
    • concurrent 模式下都为异步 concurrent 模式Demo
  • 同步:(legacy 模式下才生效)

    • 异步代码中调用(如setTimeout, Promise, MessageChannel等)

      异步代码中调用 setState,由于js的异步处理机制,异步代码会暂存,等待同步代码执行完毕再执行,此时 React 的批处理机制已经结束,因而直接更新。

    • 监听原生事件而非 React 的合成事件,在原生事件的回调函数中执行 setState 就是同步的。原因是原生事件不会触发 React 的批处理机制,因而调用 setState 会直接更新

三种模式(当前 v17)

  • legacy 模式: ReactDOM.render(<App />, rootNode)。这是当前 React app 使用的方式。当前没有计划删除本模式,但是这个模式可能不支持这些新功能。
  • blocking 模式: ReactDOM.createBlockingRoot(rootNode).render(<App />)。目前正在实验中。作为迁移到 concurrent 模式的第一个步骤。
  • concurrent 模式: ReactDOM.createRoot(rootNode).render(<App />)。目前在实验中,未来稳定之后,打算作为 React 的默认开发模式。这个模式开启了所有的新功能。
    • 可中断渲染模式。在 Concurrent 模式中,渲染不是阻塞的,优先级高的任务可以中断渲染从而优先执行
    • React 可以在不同版本的树上进行切换,一个是你屏幕上看到的那个版本,另一个是它“准备”接下来给你显示的版本
    • Concurrent 模式减少了防抖和节流在 UI 中的需求

原因

出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用。

this.setState() 被调用的时候,React 会调用 render 方法来重新渲染UI,渲染 UI 的过程其实就是操作 DOM 的过程,DOM 的操作对性能的损耗是非常严重的,所以 React 为了提高整体的渲染性能,会将一次渲染周期中的 state 进行合并,再一次性的渲染,这样可以避免频繁调用 setState 导致频繁的操作 DOM 了,从而提高渲染性能。

至于实现原理,由于代码不断的更新优化,也许以后还会更新,所以知道有这个逻辑就好,需要的时候再去阅读当前版本的代码。平时只要知道不需要担心多次进行 setState 会带来性能问题就行。

比如之前是通过 isBatchingUpdates 这个布尔属性判断是否合并更新。到后来更新 Fiber 架构后,是否同步更新的判断逻辑放进了 ReactFiberWorkLoop 中,最新又有变动,重构了 Fiber.expirationTime 并引入 Fiber.lanes源码位置

在线查看

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
    this.handleAddCount = this.handleAddCount.bind(this);
    this.btnRef = React.createRef();
  }
  componentDidMount() {
    this.handleAddCount(null, 'a');

    setTimeout(this.handleAddCount, 1 * 1000);

    Promise.resolve().then(() => {
      this.handleAddCount(null, 'b');
    });

    const button = this.btnRef.current;
    if (button) {
      button.addEventListener("click", this.handleAddCount);
    }
  }
  componentWillUnmount() {
    const button = this.btnRef.current;
    if (button) {
      button.removeEventListener("click", this.handleAddCount);
    }
  }
  handleAddCount(e, key = 'c') {
    console.log(`${key}-0`, this.state.count);
    this.setState({ count: this.state.count + 1 });
    console.log(`${key}-1`, this.state.count);
    this.setState({ count: this.state.count + 100 });
    console.log(`${key}-2`, this.state.count);
  }
  render() {
    return (
      <div>
        <p>count: {this.state.count}</p>
        <div>
          <button onClick={this.handleAddCount}>React 合成事件改变 state</button>
          <button ref={this.btnRef}>addEventListener 事件改变 state</button>
        </div>
      </div>
    );
  }
}

const domContainer = document.querySelector("#app");

// legacy 模式
ReactDOM.render(<App />, domContainer);

扩展:下面 4 次打印都是什么值?详细讨论

class Example extends React.Component {
  constructor() {
    super();
    this.state = {
      val: 0
    };
  }
  componentDidMount() {
    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 1 次 log

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 2 次 log

    setTimeout(() => {
      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 3 次 log

      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 4 次 log
    }, 0);
  }
  render() {
    return null;
  }
};

分情况:

  • legacy 模式:0 0 2 3
  • concurrent 模式:0 0 1 1

参考

@iotale iotale added the React label Mar 5, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant