React setState 是异步执行还是同步执行?

2020-08-04 10:45:34 浏览数 (1)

  1. setState 是同步更新还是异步更新?
  2. 多次调用 setState 函数,React 会不会进行合并操作?

首先是第一个问题,答:setState 有时是同步更新的,而有时却是异步更新。

一般情况下,setState 基本是异步更新,例如:

代码语言:javascript复制
// handleClick 是一个事件函数
// 当点击按钮时,count 就会  1
handleClick () {
    this.setState({
        count: this.state.count   1
    });
    // 每次点击,打印的结果总是上一次的值
    console.log(this.state.count);
}

下面的例子是 setState 同步更新的例子:

代码语言:javascript复制
clickUpdateCount () {
    this.setState({
      count: this.state.count   1
    });
}
handleClick () {
    setTimeout(() => {
        this.clickUpdateCount();
        // 打印结果与 count 一致
        console.log(this.state.count);
    },0);
}

setState 放在定时器里就会同步更新。放在自定义事件函数里也会同步更新,例如:

代码语言:javascript复制
constructor () {
    super()
    this.state = {
        count: 0
    }
    this.clickUpdateCount = this.clickUpdateCount.bind(this);
}
clickUpdateCount () {
    this.setState({
        count: this.state.count   1
    }); // 直接打印,结果与 count 一致
    console.log(this.state.count);
}
componentDidMount(){
    document.addEventListener('click', this.clickUpdateCount, false);
}
componentWillUnmount(){
    document.removeEventListener('click', this.clickUpdateCount, false);
}

如果不使用定时器或者添加自定义函数获得更新后的状态,可以给 setState 函数传入第二个参数,该参数是一个函数,它会在 state 更新完成后调用。

代码语言:javascript复制
this.setState({
    count: this.state.count   1
}, () => {
    console.log(this.state.count);
});

更新合并

一般情况下,多次调用 setState 函数 React 会把 state 对象合并到当前的 state。例如:

代码语言:javascript复制
clickUpdateCount () {
    this.setState({
      count: this.state.count   1
    });
    this.setState({
      count: this.state.count   1
    });
    this.setState({
      count: this.state.count   1
    });
}

虽然多次调用了 setState,但是点击按钮时 count 只增加 1。如果不想合并操作,可以把这些操作放在定时器当中,或者自定义事件当中。或者给 setState 的第一个参数传入函数,例如:

代码语言:javascript复制
clickUpdateCount () {
    // prevState 是更新前的 state,props 是父组件传来的属性
    this.setState(function(prevState, props) {
        return {    // 返回更新后的 state
            count: prevState.count   1
        }
    });
    this.setState(function(prevState, props) {
        return {
            count: prevState.count   1
        }
    });
    this.setState(function(prevState, props) {
        return {
            count: prevState.count   1
        }
    });
}

下图是 setState 调用时的大致流程。

图中如果条件是 true,则组件会异步更新,而如果是 false,则会同步更新。是否处于 batchUpdate,可以用一个例子来解释,比如下面的代码:

代码语言:javascript复制
class A extends React.Component{
  state = {
    count: 0
  }
  render(){
    return <div></div>
  }
  increment(){
    // 处于 batchUpdate
    // isBatchingUpdates = true
    this.setState({
      count: this.count   1
    });
    // isBatchingUpdates = false
  }
}

class B extends React.Component{
  state = {
    count: 0
  }
  render(){
    return <div></div>
  }
  increment(){
    // 处于 batchUpdate
    // isBatchingUpdates = true
    setTimeout(() => {
      this.setState({
        count: this.count   1
      });
    },0);
    // isBatchingUpdates = false
  }
}

isBatchingUpdates 的初始值是 true,当没有定时器时调用 setState 时该值还是 true,就会异步执行,而 setState 用定时器包裹后,定时器回调还没执行 isBatchingUpdates 就变成了 false,setState 就会同步执行。

props 或者 state 变化时,会调用 UpdateComponent 更新 props 和 state,然后调用 render 函数,生成新的虚拟节点(Vnode),然后 patch(oldVnode, newVnode),老的虚拟 DOM 和新的虚拟 DOM 对比更新视图。patch 分为两个阶段:

  • reconciliation 阶段:执行 diff 算法(纯 JS 计算);
  • commit 阶段:将 diff 结果渲染到 DOM 上。

由于 JavaScript 是单线程,且和 DOM 渲染共用一个线程,当组件足够复杂时,组件更新时计算量和渲染量都很大,同时再有 DOM 操作需求(比如动画、拖拽等),这可能会导致页面卡顿。

React 考虑性能优化,就把 patch 分成了两个阶段,在 reconciliation 阶段将任务拆分,拆分成多个子任务(commit 不能拆分,reconciliation 阶段是纯 JS 计算,比较好控制),当 DOM 渲染时就暂停任务,浏览器空闲时再恢复计算。通过 window.requestIdleCallback API 可以让一些任务在浏览器空闲时调用。

关于 React fiber 和 Concurrent API 可以参考这篇文章:深入剖析 React Concurrent

setState 与 useState

setState 与 useState 功能相似,在一般情况下,useState 也会对多次调用更新函数做合并处理,例如:

代码语言:javascript复制
let [count, setCount] = useState(0);
// 点击后只会相加一次
let handleClick = useCallback(() => {
    setCount(count   1);
    setCount(count   1);
},[count]);

如果 useState 更新 state 传入的也是函数时,就不会对数组做合并处理,这与 setState 行为一样。

代码语言:javascript复制
let [count, setCount] = useState(0);
let handleClick = useCallback(() => {
    // 每次点击按钮会增加 2
    setCount((prevState) => {
        return    prevState;
    });
    setCount((prevState) => {
        return    prevState;
    });
},[]);

useStatesetState 不同的是:state 是对象时,setState 会自动合并对象,而 useState 不会。例如:

代码语言:javascript复制
state = {
    count: 0,
    color: 'red'
};
handleClick(){
    this.setState({
        // 只更新了 count
        count: this.state.count   1
    });
}

render () {
    return (
      <div>
        <h1>count: { this.state.count }</h1>
        <h2>color: { this.state.color }</h2>
        <button onClick={() => this.handleClick()}>Click  1</button>
      </div>
    );
  }

运行时会发现,color 并不会丢失。而如果使用 useState,只更新 count,当点击按钮一次之后 color 就会丢失,例如:

代码语言:javascript复制
let handleClick = useCallback(() => {
    setData({
      count: data.count   1
    });
},[data.count]);

如果不让 color 丢失,可以使用这种方式:

代码语言:javascript复制
setData({
    ...data,    // 把老的对象也传进来
    count: data.count   1   // 覆盖老的值
});

setState 可以同步更新,比如在外层包裹定时器,传入第二个回调参数可以拿到更新后的数据。但 useState 是行不通的,它是异步更新,要想及时拿到更新后的数据,就需要借助 useEffect

代码语言:javascript复制
useEffect(() => {
    console.log(data.count);
},[data.count]);

useEffect 在首次渲染完成后执行一次,之后会在 data.count 的值更新后执行回调函数。

0 人点赞