React--setState 同步与异步更新

6,839 阅读5分钟

1. setState 何时同步,何时异步更新?

1.1 由 React 控制的事件处理程序,及生命周期函数中 setState 异步更新,多个 setState 可能会被合并更新。

组件 1 state 中有两个变量,num 和 times, 初始值都为1,点击 button ,分别将 num 和 times +1。

class Test1 extends Component {
  state = {
    num: 1,
    times: 1,
  }
  onClick = () => {
    console.log('state 1');
    this.setState({
      num: this.state.num + 1,
    });
    console.log('state 2');
    this.setState({
      times: this.state.times +1,
    });
    console.log('state 3');
  }

  render() {
    console.log('render');
    const { num, times } = this.state
    return (
      <div>
        组件1
        <button onClick={this.onClick}>更新</button>
        <div>
          num:{num}
        </div>
        <div>
          times:{times}
        </div>
      </div>
    );
  }
}

控制台打印结果:

image.png

setState 异步更新,并且 2 次 setState 合并更新,render 只触发 1 次。

1.2 React 控制之外的的事件中 setState 同步更新,比如原生 js 绑定事件,异步执行的 setTimeout/setInterval, Promise.then() 等。

相同的组件,在 setTimeout 中更新状态:

  onClick = () => {
    setTimeout(() => {
      console.log('state 1');
      this.setState({
        num: this.state.num + 1,
      });
      console.log('state 2');
      this.setState({
        times: this.state.times +1,
      });
      console.log('state 3');
    }, 1000);
  }

控制台打印结果:

image.png

相同的组件,在 Promise.then() 中更新状态:

  onClick = () => {
    new Promise((resolve,reject) => {
      resolve();
    }).then(() => {
      console.log('state 1');
      this.setState({
        num: this.state.num + 1,
      });
      console.log('state 2');
      this.setState({
        times: this.state.times +1,
      });
      console.log('state 3');
    })
  }

控制台打印结果:

image.png

使用原生 addEventListener 为 button 添加点击事件

  componentDidMount() {
    document.getElementById('btn').addEventListener('click',() => {
      console.log('state 1');
      this.setState({
        num: this.state.num + 1,
      });
      console.log('state 2');
      this.setState({
        times: this.state.times +1,
      });
      console.log('state 3');
    })
  }

控制台打印结果:

image.png

2. React 状态更新合并,以及如何控制异步和同步

2.1 状态更新合并

setState 可以接收两个参数,第一个参数为一个对象或者一个函数,第二个参数为一个回调函数,会在状态更新后执行,可以看做微任务。

参数为对象的 setState:

onClick = () => {
    console.log('state 1');
    this.setState({
      num: this.state.num + 1,
    })
    console.log('state 2');
    this.setState({
      num: this.state.num + 1,
    })
    console.log('state 3');
    this.setState({
      num: this.state.num + 1,
    })
    console.log('state 4');
 }

控制台打印结果:

image.png

点击按钮时执行三次 setState ,但实际 num 只增加 1 ,这是因为 setState 有“异步”性质,其中拿到的 this.num 相当于一个快照,并不能拿到即时更新的结果,三次 setState 中,this.state.num 都是 1,所以最后 num 是 2 。

参数为函数的 setState:

onClick = () => {
    console.log('state 1');
    this.setState( preState => ({
      num: preState.num + 1,
    }))
    console.log('state 2');
    this.setState( preState => ({
      num: preState.num + 1,
    }))
    console.log('state 3');
    this.setState( preState => ({
      num: preState.num + 1,
    }))
    console.log('state 4');
}

控制台打印结果:

image.png

使用函数式参数时,可以看做“同步”,preState 可以拿到即时更新后的值,经过三次更新,num 为 4 。

结合两种,以下的更新,控制台打印的结果时什么样的:

onClick = () => {
    //a
    this.setState({
      num: this.state.num + 1,
    })
    console.log('1:',this.state.num);
    //b
    this.setState({
      num: this.state.num + 1,
    })
    console.log('2:',this.state.num);
    setTimeout(() => {
      //c
      this.setState({
        num: this.state.num + 1,
      });
      console.log('3:',this.state.num);
    }, 0);
    //d
    this.setState(preState => ({
      num: preState.num + 1,
    }),() => {
      console.log('4:',this.state.num);
    })
    //e
    this.setState(preState => ({
      num: preState.num + 1,
    }))
    console.log('5:',this.state.num);
}

控制台打印结果:

image.png

更新 c 在 setTimeout 中,即使延迟时间为 0 ,也属于宏任务;其他 4 次更新会合并,所以总共实际更新两次。d 中的 log 放在回调函数中,属于微任务,所以 5 次 log 的顺序时 1, 2, 5, 4, 3 。

第一次更新中,a, b 两次 setState 中,this.state.num 都为 1 ,所以更新后 num 为 2, d, e 两次 setState 中,preState.num 都可以拿到即时更新结果,分别为 2 ,3 所以更新后 num 为 4 。

第二次更新中,this.state.num 已经是 4 了,故更新后 num 为 5。

(实际使用中不建议两种参数形式混用。)

2.2 同步异步的控制策略

在Rect 的 setState 函数的实现中,有一个变量 isBatchingUpdate ,为 true 时表示处于批量更新模式,不进行 state 的更新操作,而是将需要更新的 component 添加到 dirtyComponents 数组中;为 false 时队列执行 batchedUpdates 更新。 isBatchingUpdates 默认是 false,表示 setState 会同步更新 this.state;但是,有一个函数 batchedUpdates,该函数会把 isBatchingUpdates 修改为 true,而当 React 在调用事件处理函数之前就会先调用这个 batchedUpdates 将 isBatchingUpdates 修改为 true ,这样由 React 控制的事件处理过程 setState 不会同步更新 this.state。

image.png

2.3 将异步更新转为同步更新

在网上搜索了一下将异步更新转变为同步更新的方法,多是使用 Promise 包装一下 setState ,调用时借助 async/await 来实现同步更新状态。

   onClick = async () => {
    console.log('state 1');
    await this.setStateAsync({
      num: this.state.num + 1,
    });
    console.log('state 2');
    await this.setStateAsync({
      times: this.state.times +1,
    });
    console.log('state 3');
  }

  setStateAsync = (state) => {
    return new Promise((resolve,reject) => {
      this.setState(state,() => {
        resolve()
      })
    })
  }

控制台打印结果:

image.png

这样确实使异步更新状态转变为同步更新了,但其实不需要使用 Promise 来包装 setState ,仅仅使用 async/await 就可以实现。

onClick = async () => {
    console.log('state 1');
    this.setState({
      num: this.state.num + 1,
    });
    console.log('state 2');
    this.setState({
      num: this.state.num +1,
    });
    console.log('state 3');
    await this.setState({
      num: this.state.num +1,
    });
    console.log('state 4');
    this.setState({
      num: this.state.num +1,
    });
    console.log('state 5');
    this.setState({
      num: this.state.num +1,
    });
    console.log('state 6');
 }

控制台打印结果:

image.png

可以看出,前三次 setState 依然是异步更新,并且合并更新了,在第三次的 setState 使用了await 之后,后两次 setState 都是同步更新。

甚至都不需要等待 setState ,任意等待一个 Promise 也有这样的效果:

  onClick = async () => {
    console.log('state 1');
    this.setState({
      num: this.state.num + 1,
    });
    console.log('state 2');
    this.setState({
      num: this.state.num +1,
    });
    console.log('state 3');
    this.setState({
      num: this.state.num +1,
    });

    await new Promise((resolve,reject) => {
      console.log('promise start');
      resolve();
    })
    console.log('promise end');

    console.log('state 4');
    this.setState({
      num: this.state.num +1,
    });
    console.log('state 5');
    this.setState({
      num: this.state.num +1,
    });
    console.log('state 6');
  }

控制台打印结果:

image.png

await 之前的三次 setState 异步更新了,await 之后的两次 setState 同步更新。

首先我们要理解 async/await 。 async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句( await 通常用来等待一个 Promise ,但是也可以等待一般的函数表达式,等待一般表达式时相当于使用 Promise.resolve() 包装其返回值)。也就是说函数体中 await 后的语句都是异步触发,此时已经脱离了 React 的调度,所以 setState 变成了同步更新。

附一道简单的 async/await 面试题,加深一下对 async/await 的理解:

async function async1(){
  console.log('async1 start');
  let res = await async2();
  console.log(res);
  console.log('async1 end');
}

async function async2(){
  console.log('async2 start');
  return 'async2 end'
}

console.log('script start');
setTimeout(() => {
  console.log('setTimeout');
}, 0);
async1();
new Promise((resolve,reject) => {
  console.log('Promise start');
  resolve();
}).then(() => {
  console.log('Promise end');
})
console.log('script end');

控制台打印结果:

image.png

主线程首先打印 'script start' 后遇到 setTimeout ,函数体进入宏任务;

执行函数 async1 ,打印 'async1 start', 遇到 await ,进入函数 async2,打印 'async2 start', 返回值 'async2 end' 会被 Promsie.resolve() 包装,进入微任务;

进入 new Promsie(), 打印 'Promise start', resolve('Promise end') 进入微任务

打印 'script end' ,主线程结束。

微任务1, 函数 async2 返回, 函数 async2 等待结束,打印 'async2 end', 'async1 end'。

微任务2,Promise.then(), 打印 'Promise end'。微任务清空。

执行宏任务,打印 'setTimeout'。

参考

React中的setState的同步异步与合并

React 中setState更新state何时同步何时异步?